Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions python/src/plugins/headless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Crossmint Headless Checkout Plugin for GOAT SDK

A plugin for the GOAT SDK that enables seamless purchasing of Amazon items and other tokenized products using cryptocurrency through Crossmint's headless checkout API.

## Installation

```bash
# Install the plugin
poetry add goat-sdk-plugin-crossmint-headless-checkout

# Install optional wallet dependencies for chain-specific operations
poetry add goat-sdk-wallet-evm
poetry add goat-sdk-wallet-solana
```

## Usage

```python
from goat_plugins.crossmint_headless_checkout import crossmint_headless_checkout, CrossmintHeadlessCheckoutPluginOptions

# Initialize the plugin
options = CrossmintHeadlessCheckoutPluginOptions(
api_key="your-crossmint-api-key"
)
plugin = crossmint_headless_checkout(options)

# Example: Purchase an Amazon product
order = await plugin.buy_token(
recipient={
"email": "[email protected]",
"physicalAddress": {
"name": "John Doe",
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
},
payment={
"method": "base-sepolia",
"currency": "usdc",
"payerAddress": "0x1234567890123456789012345678901234567890",
"receiptEmail": "[email protected]"
},
lineItems=[{
"productLocator": "amazon:B08SVZ775L",
"callData": {"totalPrice": "29.99"}
}],
locale="en-US"
)
```

## Features

- **Amazon Product Purchase**: Buy Amazon items using cryptocurrency
- **Multi-chain Support**: Compatible with EVM chains (Ethereum, Base, Polygon) and Solana
- **Payment Methods**: Support for USDC, ETH, SOL, and other cryptocurrencies
- **Global Shipping**: Handle international shipping addresses and billing
- **Order Management**: Track order status and transaction details

### Available Tools

**Plugin Tools:**
- `buy_token`: Purchase tokenized products (Amazon items, NFTs, etc.) using cryptocurrency

**Wallet Core Tools:**
- `get_address`: Get the wallet's public address
- `get_chain`: Get information about the blockchain network
- `get_balance`: Get balance for native currency or specific tokens
- `get_token_info_by_ticker`: Get information about a token by its ticker symbol (e.g., USDC, SOL)
- `convert_to_base_units`: Convert token amounts from human-readable to base units
- `convert_from_base_units`: Convert token amounts from base units to human-readable format

**Additional EVM Tools** (when using EVM wallets):
- `get_token_allowance_evm`: Check ERC20 token allowances for spenders
- `sign_typed_data_evm`: Sign EIP-712 typed data structures
- `send_token`: Send native currency (ETH) or ERC20 tokens (when sending is enabled)
- `approve_token_evm`: Approve ERC20 token spending allowances (when sending is enabled)
- `revoke_token_approval_evm`: Revoke ERC20 token approvals (when sending is enabled)

**Additional Solana Tools** (when using Solana wallets):
- `send_token`: Send native SOL or SPL tokens (when sending is enabled)

### Supported Payment Methods
- **EVM Chains**: USDC, ETH on Ethereum, Base, Polygon
- **Solana**: USDC-SPL, SOL, and other SPL tokens

## License

This project is licensed under the terms of the MIT license.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from dataclasses import dataclass
from goat.classes.plugin_base import PluginBase
from .service import CrossmintHeadlessCheckoutService


@dataclass
class CrossmintHeadlessCheckoutPluginOptions:
"""Options for the CrossmintHeadlessCheckoutPlugin."""
api_key: str # API key for external service integration


class CrossmintHeadlessCheckoutPlugin(PluginBase):
"""
Plugin for Crossmint's Headless Checkout API.

This plugin allows purchasing of various items (NFTs, physical products, etc.)
through Crossmint's APIs using cryptocurrency.
"""
def __init__(self, options: CrossmintHeadlessCheckoutPluginOptions):
super().__init__("crossmint-headless-checkout", [CrossmintHeadlessCheckoutService(options.api_key)])

def supports_chain(self, chain) -> bool:
"""
This plugin supports all chains, since Crossmint can facilitate transactions on various chains.

Args:
chain: The chain to check support for

Returns:
bool: Always True, as Crossmint supports multiple chains
"""
# Support all chains, just like in the TypeScript implementation
return True


def crossmint_headless_checkout(options: CrossmintHeadlessCheckoutPluginOptions) -> CrossmintHeadlessCheckoutPlugin:
"""
Create a new instance of the CrossmintHeadlessCheckoutPlugin.

Args:
options: Configuration options for the plugin

Returns:
An instance of CrossmintHeadlessCheckoutPlugin
"""
return CrossmintHeadlessCheckoutPlugin(options)
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from pydantic import BaseModel, Field, validator, root_validator
from typing import List, Optional, Union, Dict, Any
import re


class PhysicalAddress(BaseModel):
name: str = Field(description="Full name of the recipient")
line1: str = Field(description="Street address, P.O. box, company name, c/o")
line2: Optional[str] = Field(None, description="Apartment, suite, unit, building, floor, etc.")
city: str = Field(description="City, district, suburb, town, or village")
state: Optional[str] = Field(None, description="State/Province/Region - optional")
postalCode: str = Field(description="ZIP or postal code")
country: str = Field(description="Two-letter country code (ISO 3166-1 alpha-2)")

@validator('name')
def validate_name(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError("Name is required for physical address")
return v

@validator('line1')
def validate_line1(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError("Line 1 is required for physical address")
return v

@validator('city')
def validate_city(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError("City is required for physical address")
return v

@validator('postalCode')
def validate_postal_code(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError("Postal/ZIP code is required for physical address")
return v

@validator('country')
def validate_country(cls, v):
if not v:
raise ValueError("Country is required for physical address")

# Convert to uppercase
v = v.upper()

# Validate length (must be exactly 2 characters for ISO 3166-1 alpha-2)
if len(v) < 2:
raise ValueError("Country must be a 2-letter ISO code for physical address")
if len(v) > 2:
raise ValueError("Country must be a 2-letter ISO code for physical address")

# Removed the US-only restriction - let the API handle country validation
return v

@root_validator(skip_on_failure=True)
def validate_state_for_us(cls, values):
country = values.get('country')
state = values.get('state')

# State is required for US addresses, but don't enforce for other countries
if country == "US" and not state:
raise ValueError("State is required for US physical address")

return values


class Recipient(BaseModel):
email: str = Field(description="Email address for the recipient")
physicalAddress: Optional[PhysicalAddress] = Field(
None,
description="Physical shipping address for the recipient. Required when purchasing physical products."
)

@validator('email')
def validate_email(cls, v):
if not v:
raise ValueError("Email is required")

# Basic email validation regex that matches common email formats
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, v):
raise ValueError("value is not a valid email address")

return v


class Payment(BaseModel):
method: str = Field(
description="The blockchain network to use for the transaction (e.g., 'ethereum', 'ethereum-sepolia', 'base', 'base-sepolia', 'polygon', 'polygon-amoy', 'solana')"
)
currency: str = Field(
description="The currency to use for payment (e.g., 'usdc' or 'eth' or 'sol')"
)
payerAddress: str = Field(
description="The address that will pay for the transaction"
)
receiptEmail: Optional[str] = Field(
None,
description="Optional email to send payment receipt to"
)

@validator('method')
def validate_method(cls, v):
allowed_methods = ["ethereum", "ethereum-sepolia", "base", "base-sepolia", "polygon", "polygon-amoy", "solana"]
if v not in allowed_methods:
raise ValueError(f"Method must be one of: {', '.join(allowed_methods)}")
return v

@validator('currency')
def validate_currency(cls, v):
allowed_currencies = ["usdc", "eth", "sol"]
if v not in allowed_currencies:
raise ValueError(f"Currency must be one of: {', '.join(allowed_currencies)}")
return v


class CollectionLineItem(BaseModel):
collectionLocator: str = Field(
description="The collection locator. Ex: 'crossmint:<crossmint_collection_id>', '<chain>:<contract_address>'"
)
callData: Optional[Dict[str, Any]] = None


class ProductLineItem(BaseModel):
productLocator: str = Field(
description="The product locator. Ex: 'amazon:<amazon_product_id>', 'amazon:<asin>'"
)
callData: Optional[Dict[str, Any]] = None


class BuyTokenParameters(BaseModel):
recipient: Recipient = Field(
description="Where the tokens will be sent to - either a wallet address or email, if email is provided a Crossmint wallet will be created and associated with the email"
)
locale: str = Field(
default="en-US",
description="The locale for the order (e.g., 'en-US')"
)
payment: Payment = Field(
description="Payment configuration - the desired blockchain, currency and address of the payer - optional receipt email, if an email recipient was not provided"
)
lineItems: List[Union[CollectionLineItem, ProductLineItem]] = Field(
description="Array of items to purchase"
)


class GetOrderParameters(BaseModel):
order_id: str = Field(
description="The unique identifier of the order to retrieve"
)
Loading