@mysten/payment-kit
Advanced tools
| # Getting Started | ||
| > Install and set up Payment Kit for your Sui application. | ||
| > **Note:** This package is in active development and should be used with caution. APIs are experimental and | ||
| > subject to breaking changes without notice. We recommend thoroughly testing any implementation | ||
| > before using in production environments. | ||
| This guide will help you set up and start using the Payment Kit SDK in your Sui application. | ||
| ## Prerequisites | ||
| Before you begin, ensure you have: | ||
| - A Sui wallet with Testnet or Mainnet SUI tokens | ||
| - Basic understanding of TypeScript and the Sui blockchain | ||
| ## Installation | ||
| Install the Payment Kit SDK and the Sui TypeScript SDK: | ||
| ```bash npm2yarn | ||
| npm install --save @mysten/payment-kit @mysten/sui | ||
| ``` | ||
| ## Setting up the client | ||
| The Payment Kit SDK extends the standard `SuiGrpcClient` to provide payment functionality. Here's | ||
| how to set it up: | ||
| ```ts | ||
| // Create a Sui client with Payment Kit extension | ||
| const client = new SuiGrpcClient({ | ||
| network: 'testnet', | ||
| baseUrl: 'https://fullnode.testnet.sui.io:443', | ||
| }).$extend(paymentKit()); | ||
| ``` | ||
| The client will automatically configure the correct package IDs for the network you're using | ||
| (Testnet or Mainnet). | ||
| ## Your first payment | ||
| Let's process a basic registry-based payment using the default payment registry: | ||
| ```ts | ||
| // Create or load your keypair | ||
| const keypair = Ed25519Keypair.generate(); | ||
| const sender = keypair.getPublicKey().toSuiAddress(); | ||
| // Create the payment transaction | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), // Unique identifier for this payment | ||
| coinType: '0x2::sui::SUI', // Coin type (SUI in this case) | ||
| amount: 1n * MIST_PER_SUI, // 1 SUI (in MIST) | ||
| receiver, | ||
| sender: sender, | ||
| }); | ||
| // Sign and execute | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEffects: true, | ||
| showEvents: true, | ||
| }, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Payment processed:', result.Transaction.digest); | ||
| ``` | ||
| ## Understanding the payment | ||
| In this example: | ||
| 1. **Nonce**: A unique identifier (UUIDv4) that prevents duplicate payments when using a registry | ||
| 2. **Coin Type**: The type of coin being transferred (SUI token) | ||
| 3. **Amount**: The payment amount in the smallest unit (MIST for SUI) | ||
| 4. **Receiver**: The address receiving the payment | ||
| 5. **Sender**: The address sending the payment (must match the transaction signer) | ||
| 6. **Registry Name**: The payment registry to use (defaults to `DEFAULT_REGISTRY_NAME`) | ||
| ## Verifying the payment | ||
| After processing a payment, you can query the payment record to verify it exists: | ||
| ```ts | ||
| // Query the payment record | ||
| const paymentRecord = await client.paymentKit.getPaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1n * MIST_PER_SUI, | ||
| receiver, | ||
| }); | ||
| if (paymentRecord) { | ||
| console.log('Payment verified!'); | ||
| console.log('Transaction:', paymentRecord.paymentTransactionDigest); | ||
| console.log('Epoch:', paymentRecord.epochAtTimeOfRecord); | ||
| } else { | ||
| console.log('Payment not found'); | ||
| } | ||
| ``` | ||
| ## Processing an ephemeral payment | ||
| If you don't need duplicate prevention or persistent storage, use ephemeral payments: | ||
| ```ts | ||
| const tx = client.paymentKit.tx.processEphemeralPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1n * MIST_PER_SUI, | ||
| receiver, | ||
| sender: sender, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Ephemeral payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Ephemeral payment processed:', result.Transaction.digest); | ||
| ``` | ||
| Ephemeral payments: | ||
| - Don't create a `PaymentRecord` | ||
| - Don't prevent duplicates | ||
| - Still emit a `PaymentReceipt` event | ||
| - Have lower gas costs | ||
| ## Working with different coin types | ||
| Payment Kit supports any Sui coin type. Here's how to process payments with custom coins: | ||
| ```ts | ||
| // Example with a custom coin type | ||
| const customCoinType = '0xabcd...::my_coin::MY_COIN'; | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: customCoinType, | ||
| amount: 5000, | ||
| receiver, | ||
| sender: sender, | ||
| }); | ||
| // Execute the transaction | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ## Error handling | ||
| Always wrap payment operations in try-catch blocks: | ||
| ```ts | ||
| try { | ||
| const tx = client.paymentKit.tx.processRegistryPayment({...}); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Success:', result.Transaction.digest); | ||
| } catch (error) { | ||
| if (error.message.includes('Duplicate')) { | ||
| console.error('Payment already processed'); | ||
| } else if (error.message.includes('insufficient')) { | ||
| console.error('Insufficient balance'); | ||
| } else { | ||
| console.error('Payment failed:', error.message); | ||
| } | ||
| } | ||
| ``` | ||
| ## Creating payment URIs | ||
| Payment Kit provides helper functions to create and parse Payment Kit transaction URIs. These URIs | ||
| are useful for generating payment links, QR codes, or deep links in your application: | ||
| ```ts | ||
| // Create a payment URI | ||
| const paymentUri = createPaymentTransactionUri({ | ||
| receiverAddress: '0x123...abc', | ||
| amount: 1000000000n, // 1 SUI in MIST | ||
| coinType: '0x2::sui::SUI', | ||
| nonce: crypto.randomUUID(), | ||
| label: 'Coffee Payment', | ||
| message: 'Payment for espresso', | ||
| registryName: 'my-registry', // Optional: specify a registry name | ||
| // Or use registryId: '0x...' for a specific registry object ID | ||
| }); | ||
| console.log(paymentUri); | ||
| // Output: sui:pay?receiver=0x123...abc&amount=1000000000&coinType=0x2%3A%3Asui%3A%3ASUI&nonce=...&label=Coffee%20Payment&message=Payment%20for%20espresso®istry=my-registry | ||
| ``` | ||
| The URI parameters include: | ||
| - **receiver**: The Sui address receiving the payment (required) | ||
| - **amount**: The payment amount as a positive bigint (required) | ||
| - **coinType**: The coin type being transferred (required) | ||
| - **nonce**: A unique identifier up to 36 characters (required) | ||
| - **registry**: Either a registry name or object ID (optional) | ||
| - **label**: A short label for the payment (optional) | ||
| - **message**: A longer description message (optional) | ||
| - **iconUrl**: URL to an icon for display purposes (optional) | ||
| You can then parse URIs back into payment parameters: | ||
| ```ts | ||
| // Parse a payment URI | ||
| const params = parsePaymentTransactionUri(paymentUri); | ||
| // Returns: { receiverAddress, amount, coinType, nonce, label?, message?, iconUrl?, registryName? | registryId? } | ||
| // Use the parsed parameters to create a transaction | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| receiver: params.receiverAddress, | ||
| amount: params.amount, | ||
| coinType: params.coinType, | ||
| nonce: params.nonce, | ||
| registryName: params.registryName, | ||
| sender: sender, | ||
| }); | ||
| ``` | ||
| ## Next steps | ||
| Now that you understand the basics, explore more advanced features: | ||
| - [Payment Processing](./payment-processing) - Deep dive into payment models | ||
| - [Registry Management](./registry-management) - Create and configure custom registries | ||
| - [SDK API Reference](./payment-kit-sdk) - Complete SDK API documentation |
+132
| # Payment Kit | ||
| > Accept payments in any coin type on the Sui blockchain with built-in duplicate prevention. | ||
| > **Note:** This package is in active development and should be used with caution. APIs are experimental and | ||
| > subject to breaking changes without notice. We recommend thoroughly testing any implementation | ||
| > before using in production environments. | ||
| The Sui Payment Kit SDK is a TypeScript library that provides a straightforward and secure way to | ||
| process blockchain payments on the Sui network. It offers flexible payment processing with built-in | ||
| duplicate prevention, configurable payment registries, and comprehensive receipt management. | ||
| ## Overview | ||
| Payment Kit enables developers to integrate secure payment processing into their Sui applications | ||
| with minimal setup. The SDK provides two payment models: | ||
| - **Registry-based payments**: Persistent payment records with duplicate prevention and configurable | ||
| expiration | ||
| - **Ephemeral payments**: Lightweight payment processing without persistent storage | ||
| All payments emit a `PaymentReceipt` that can be stored offchain for verification and | ||
| record-keeping. | ||
| ## Key features | ||
| - **Duplicate Prevention**: Registry-based payments automatically prevent duplicate transactions | ||
| - **Flexible Payment Models**: Choose between registry-based or ephemeral payment processing | ||
| - **Payment Registries**: Create and manage custom payment registries with configurable policies | ||
| - **Receipt Management**: Automatic receipt generation for all payments | ||
| - **Multi-coin Support**: Process payments with any Sui coin type | ||
| - **Configurable Expiration**: Set custom expiration policies for payment records | ||
| - **Fund Management**: Optional registry-managed funds for simplified coin handling | ||
| ## Installation | ||
| ```bash npm2yarn | ||
| npm install --save @mysten/payment-kit @mysten/sui | ||
| ``` | ||
| ## Quick start | ||
| ```ts | ||
| // Create a Sui client with a Payment Kit extension | ||
| const client = new SuiGrpcClient({ | ||
| network: 'testnet', | ||
| baseUrl: 'https://fullnode.testnet.sui.io:443', | ||
| }).$extend(paymentKit()); | ||
| // Process a registry-based payment | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1n * MIST_PER_SUI, // 1 SUI in MIST | ||
| receiver: '0x123...abc', | ||
| sender: '0x456...def', | ||
| }); | ||
| // Sign and execute the transaction | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| ``` | ||
| ## Network support | ||
| Payment Kit supports the following Sui networks: | ||
| - **Mainnet**: Production environment | ||
| - **Testnet**: Testing environment | ||
| The SDK automatically configures the correct package and object IDs based on the network specified | ||
| in your `SuiGrpcClient` configuration. | ||
| ## Core concepts | ||
| ### Payment processing | ||
| Payment Kit offers two distinct payment processing models to suit different application needs: | ||
| 1. **Registry-based Payments**: Creates a persistent `PaymentRecord` that prevents duplicate | ||
| payments and provides verifiable proof of payment | ||
| 2. **Ephemeral Payments**: Processes payments without persistent storage, suitable for scenarios | ||
| where duplicate prevention is handled externally | ||
| ### Payment registries | ||
| A `PaymentRegistry` manages payment records and configurations. Benefits include: | ||
| - Centralized payment tracking | ||
| - Configurable expiration policies | ||
| - Optional fund management | ||
| - Custom naming for easy identification and indexing | ||
| ### Payment records | ||
| When using registry-based payments, a `PaymentRecord` is created as a Dynamic Field on the registry. | ||
| Records are identified by a composite key derived from: | ||
| - Payment nonce (unique identifier) | ||
| - Payment amount | ||
| - Coin type | ||
| - Receiver address | ||
| ### Payment receipts | ||
| Every payment (registry-based or ephemeral) emits a `PaymentReceipt` event containing: | ||
| ```ts | ||
| type PaymentReceipt = { | ||
| paymentType: 'Registry' | 'Ephemeral'; | ||
| nonce: string; | ||
| amount: number; | ||
| receiver: string; | ||
| coinType: string; | ||
| timestampMs: number; | ||
| }; | ||
| ``` | ||
| ## Next steps | ||
| - [Getting Started Guide](/payment-kit/getting-started) - Detailed setup and usage instructions | ||
| - [Payment Processing](/payment-kit/payment-processing) - Learn about different payment models | ||
| - [Registry Management](/payment-kit/registry-management) - Create and configure payment registries | ||
| - [SDK API Reference](/payment-kit/payment-kit-sdk) - Complete SDK API documentation |
| # Payment Kit | ||
| > Accept payments in any coin type on Sui | ||
| - [Payment Kit](..md): Accept payments in any coin type on the Sui blockchain with built-in duplicate prevention. | ||
| - [Getting Started](./getting-started.md): Install and set up Payment Kit for your Sui application. | ||
| - [Payment Kit SDK](./payment-kit-sdk.md): Payment Kit SDK API reference and configuration for the PaymentKitClient. | ||
| - [Payment Processing](./payment-processing.md): Process registry-based and ephemeral payments and handle transaction results on Sui. | ||
| - [Registry Management](./registry-management.md): Create, configure, and manage payment registries and accepted coin types on Sui. |
| # Payment Kit SDK | ||
| > Payment Kit SDK API reference and configuration for the PaymentKitClient. | ||
| > **Note:** This package is in active development and should be used with caution. APIs are experimental and | ||
| > subject to breaking changes without notice. We recommend thoroughly testing any implementation | ||
| > before using in production environments. | ||
| Complete API reference for the Payment Kit SDK. | ||
| ## Client setup | ||
| ### PaymentKitClient | ||
| The main client class that provides access to Payment Kit functionality. | ||
| #### `paymentKit()` | ||
| Function to create a Payment Kit client extension for `SuiGrpcClient`. | ||
| ```ts | ||
| paymentKit<Name extends string = 'paymentKit'>({ | ||
| name = 'paymentKit' as Name, | ||
| }): SuiClientRegistration<PaymentKitCompatibleClient, Name, PaymentKitClient> | ||
| ``` | ||
| **Example:** | ||
| ```ts | ||
| const client = new SuiGrpcClient({ | ||
| network: 'testnet', | ||
| baseUrl: 'https://fullnode.testnet.sui.io:443', | ||
| }).$extend(paymentKit()); | ||
| // Access Payment Kit functionality | ||
| client.paymentKit.tx.processRegistryPayment(/* ... */); | ||
| ``` | ||
| **Supported Networks:** | ||
| - `testnet` | ||
| - `mainnet` | ||
| **Throws:** | ||
| - `PaymentKitClientError` if network is unsupported | ||
| ## Transaction methods | ||
| Transaction methods create complete, ready-to-sign transactions. | ||
| ### `client.paymentKit.tx.processRegistryPayment()` | ||
| Create a transaction to process a registry-based payment. | ||
| ```ts | ||
| processRegistryPayment( | ||
| options: ProcessRegistryPaymentOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type ProcessRegistryPaymentOptions = { | ||
| nonce: string; // Unique payment identifier | ||
| coinType: string; // Coin type (e.g., '0x2::sui::SUI') | ||
| amount: number | bigint; // Amount in smallest unit | ||
| receiver: string; // Recipient address | ||
| sender: string; // Sender address (must match signer) | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| If you prefer to provide the `registryId`, you can do so instead of the `registryName`. | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| ``` | ||
| **On-Chain Effects:** | ||
| - Transfers coins from sender to receiver | ||
| - Creates a `PaymentRecord` dynamic field on the registry | ||
| - Emits a `PaymentReceipt` event | ||
| **Throws:** | ||
| - Error if payment with same key already exists | ||
| - Error if sender has insufficient balance | ||
| --- | ||
| ### `client.paymentKit.tx.processEphemeralPayment()` | ||
| Create a transaction to process an ephemeral payment (no registry storage). | ||
| ```ts | ||
| processEphemeralPayment( | ||
| options: ProcessEphemeralPaymentOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type ProcessEphemeralPaymentOptions = { | ||
| nonce: string; // Payment identifier (not enforced for uniqueness) | ||
| coinType: string; // Coin type | ||
| amount: number | bigint; // Amount in smallest unit | ||
| receiver: string; // Recipient address | ||
| sender: string; // Sender address (must match signer) | ||
| }; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.processEphemeralPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 500000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Ephemeral payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| ``` | ||
| **On-Chain Effects:** | ||
| - Transfers coins from sender to receiver | ||
| - Emits a `PaymentReceipt` event | ||
| - No `PaymentRecord` created (no duplicate prevention) | ||
| --- | ||
| ### `client.paymentKit.tx.createRegistry()` | ||
| Create a transaction to create a new payment registry. | ||
| ```ts | ||
| createRegistry(options: CreateRegistryOptions): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type CreateRegistryOptions = { | ||
| registryName: string; // Unique name for the registry | ||
| }; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.createRegistry({ | ||
| registryName: 'my-payment-registry', | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showObjectChanges: true, | ||
| }, | ||
| }); | ||
| // Extract the RegistryAdminCap from result.Transaction.objectChanges | ||
| const adminCap = result.Transaction.objectChanges?.find( | ||
| (change) => change.type === 'created' && change.objectType.includes('RegistryAdminCap'), | ||
| ); | ||
| ``` | ||
| **On-Chain Effects:** | ||
| - Creates a `PaymentRegistry` object | ||
| - Creates a `RegistryAdminCap` owned by sender | ||
| - Registry ID is deterministically derived from name | ||
| **Important:** | ||
| - Registry names must be unique globally | ||
| - Save the `RegistryAdminCap` object ID for configuration operations | ||
| --- | ||
| ### `client.paymentKit.tx.setConfigEpochExpirationDuration()` | ||
| Create a transaction to set the epoch expiration duration for payment records. | ||
| ```ts | ||
| setConfigEpochExpirationDuration( | ||
| options: SetEpochExpirationDurationOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type SetEpochExpirationDurationOptions = { | ||
| epochExpirationDuration: number | bigint; // Number of epochs before records expire | ||
| adminCapId: string; // Admin capability object ID | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.setConfigEpochExpirationDuration({ | ||
| registryName: 'my-registry', | ||
| epochExpirationDuration: 60, | ||
| adminCapId: adminCapId, | ||
| }); | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| **Requires:** | ||
| - Transaction signer must own the admin capability | ||
| **Default:** 30 epochs | ||
| --- | ||
| ### `client.paymentKit.tx.setConfigRegistryManagedFunds()` | ||
| Create a transaction to enable/disable registry-managed funds. | ||
| ```ts | ||
| setConfigRegistryManagedFunds( | ||
| options: SetRegistryManagedFundsOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type SetRegistryManagedFundsOptions = { | ||
| registryManagedFunds: boolean; // Enable (true) or disable (false) | ||
| adminCapId: string; // Admin capability object ID | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.setConfigRegistryManagedFunds({ | ||
| registryName: 'my-registry', | ||
| registryManagedFunds: true, | ||
| adminCapId: adminCapId, | ||
| }); | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| **When Enabled:** | ||
| - Payment receiver must be the registry itself | ||
| - Funds accumulate in the registry | ||
| - Admin can withdraw funds later | ||
| **Default:** Disabled (funds go directly to receivers) | ||
| --- | ||
| ### `client.paymentKit.tx.withdrawFromRegistry()` | ||
| Create a transaction to withdraw accumulated funds from a registry. | ||
| ```ts | ||
| withdrawFromRegistry( | ||
| options: WithdrawFromRegistryOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type WithdrawFromRegistryOptions = { | ||
| coinType: string; // Coin type to withdraw | ||
| adminCapId: string; // Admin capability object ID | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.withdrawFromRegistry({ | ||
| coinType: '0x2::sui::SUI', | ||
| registryName: 'my-registry', | ||
| adminCapId: adminCapId, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| **Requires:** | ||
| - Registry must have `registryManagedFunds` enabled | ||
| - Transaction signer must own the admin capability | ||
| **Effect:** | ||
| - Withdraws all coins of specified type to the sender | ||
| --- | ||
| ### `client.paymentKit.tx.deletePaymentRecord()` | ||
| Create a transaction to delete an expired payment record. | ||
| ```ts | ||
| deletePaymentRecord( | ||
| options: DeletePaymentRecordOptions | ||
| ): Transaction | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type DeletePaymentRecordOptions = { | ||
| nonce: string; // Payment nonce | ||
| coinType: string; // Coin type of the payment | ||
| amount: number | bigint; // Payment amount | ||
| receiver: string; // Payment receiver | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `Transaction` - Ready-to-sign transaction | ||
| **Example:** | ||
| ```ts | ||
| const tx = client.paymentKit.tx.deletePaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| registryName: 'my-registry', | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| **Requires:** | ||
| - Payment record must be expired (current epoch >= record epoch + expiration duration) | ||
| - Anyone can delete expired records (permissionless) | ||
| **Effect:** | ||
| - Removes the payment record from the registry | ||
| - Sender receives storage rebate | ||
| ## Query methods | ||
| ### `client.paymentKit.getPaymentRecord()` | ||
| Query the blockchain for a payment record. | ||
| ```ts | ||
| async getPaymentRecord( | ||
| options: GetPaymentRecordOptions | ||
| ): Promise<GetPaymentRecordResponse | null> | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type GetPaymentRecordOptions = { | ||
| nonce: string; // Payment nonce | ||
| coinType: string; // Coin type | ||
| amount: number | bigint; // Payment amount | ||
| receiver: string; // Payment receiver | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** | ||
| ```ts | ||
| type GetPaymentRecordResponse = { | ||
| key: string; // Dynamic field object ID | ||
| paymentTransactionDigest: string | null; // Transaction that created the record | ||
| epochAtTimeOfRecord: string; // Epoch when record was created | ||
| } | null; // null if record doesn't exist | ||
| ``` | ||
| **Example:** | ||
| ```ts | ||
| const record = await client.paymentKit.getPaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| }); | ||
| if (record) { | ||
| console.log('Payment exists!'); | ||
| console.log('Transaction:', record.paymentTransactionDigest); | ||
| console.log('Created at epoch:', record.epochAtTimeOfRecord); | ||
| } else { | ||
| console.log('Payment not found'); | ||
| } | ||
| ``` | ||
| **Use Cases:** | ||
| - Verify a payment was completed | ||
| - Check for duplicate payments before processing | ||
| - Retrieve payment details for reconciliation | ||
| ## Call methods | ||
| Call methods return transaction commands that can be composed in custom transactions. Use these when | ||
| you need fine-grained control over transaction construction. | ||
| ### `client.paymentKit.calls.processRegistryPayment()` | ||
| Returns a transaction command for registry-based payment. | ||
| ```ts | ||
| processRegistryPayment( | ||
| options: ProcessRegistryPaymentOptions | ||
| ): TransactionObjectArgument | ||
| ``` | ||
| **Example:** | ||
| ```ts | ||
| const tx = new Transaction(); | ||
| // Add custom logic before payment | ||
| tx.moveCall({ | ||
| target: '0xMyPackage::my_module::pre_payment_check', | ||
| arguments: [ | ||
| /* ... */ | ||
| ], | ||
| }); | ||
| // Add payment command | ||
| tx.add( | ||
| client.paymentKit.calls.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }), | ||
| ); | ||
| // Add custom logic after payment | ||
| tx.moveCall({ | ||
| target: '0xMyPackage::my_module::post_payment_action', | ||
| arguments: [ | ||
| /* ... */ | ||
| ], | ||
| }); | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ### Other call methods | ||
| All transaction methods have corresponding call methods: | ||
| - `client.paymentKit.calls.processEphemeralPayment()` | ||
| - `client.paymentKit.calls.createRegistry()` | ||
| - `client.paymentKit.calls.setConfigEpochExpirationDuration()` | ||
| - `client.paymentKit.calls.setConfigRegistryManagedFunds()` | ||
| - `client.paymentKit.calls.withdrawFromRegistry()` | ||
| - `client.paymentKit.calls.deletePaymentRecord()` | ||
| ## Types | ||
| ### PaymentReceipt event | ||
| Emitted when a payment is processed (registry or ephemeral). | ||
| ```ts | ||
| type PaymentReceipt = { | ||
| payment_type: 'Registry' | 'Ephemeral'; // Payment model used | ||
| nonce: string; // Payment identifier | ||
| amount: string; // Amount (as string) | ||
| receiver: string; // Recipient address | ||
| coin_type: string; // Coin type | ||
| timestamp_ms: string; // Unix timestamp in ms | ||
| }; | ||
| ``` | ||
| **Extracting from Transaction Result:** | ||
| ```ts | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEvents: true, | ||
| }, | ||
| }); | ||
| const receiptEvent = result.Transaction.events?.find((event) => | ||
| event.type.includes('PaymentReceipt'), | ||
| ); | ||
| if (receiptEvent) { | ||
| const receipt = receiptEvent.parsedJson as PaymentReceipt; | ||
| console.log('Payment Type:', receipt.payment_type); | ||
| console.log('Nonce:', receipt.nonce); | ||
| console.log('Amount:', receipt.amount); | ||
| } | ||
| ``` | ||
| ## Error handling | ||
| ### PaymentKitClientError | ||
| Custom error class for Payment Kit errors. | ||
| ```ts | ||
| class PaymentKitClientError extends Error { | ||
| constructor(message: string); | ||
| } | ||
| ``` | ||
| ## URI utilities | ||
| Payment Kit provides utility functions for creating and parsing payment transaction URIs. These URIs | ||
| follow the `sui:pay` protocol and can be used to encode payment parameters for QR codes, deep links, | ||
| or other sharing mechanisms. | ||
| ### URI format | ||
| Payment URIs follow the `sui:pay` protocol: | ||
| ``` | ||
| sui:pay? | ||
| receiver=<address> | ||
| &amount=<amount> | ||
| &coinType=<coinType> | ||
| &nonce=<nonce> | ||
| ®istry=<registryIdOrName> | ||
| &label=<label> | ||
| &message=<message> | ||
| &iconUrl=<url> | ||
| ``` | ||
| **Required Parameters:** | ||
| | Parameter | Description | | ||
| | ---------- | --------------------------------------------------- | | ||
| | `receiver` | Recipient Sui address | | ||
| | `amount` | Payment amount in smallest unit (for example, MIST) | | ||
| | `coinType` | Coin type (for example, `0x2::sui::SUI`) | | ||
| | `nonce` | Unique payment identifier (max 36 chars) | | ||
| **Optional Parameters:** | ||
| | Parameter | Description | | ||
| | ---------- | ----------------------------------------------- | | ||
| | `registry` | Registry ID or name for registry-based payments | | ||
| | `label` | Human-readable label for the payment | | ||
| | `message` | Message or memo for the payment | | ||
| | `iconUrl` | URL to an icon image for display purposes | | ||
| --- | ||
| ### `createPaymentTransactionUri()` | ||
| Creates a payment transaction URI from the given parameters. | ||
| ```ts | ||
| createPaymentTransactionUri(params: PaymentUriParams): string | ||
| ``` | ||
| **Parameters:** | ||
| ```ts | ||
| type PaymentUriParams = { | ||
| receiverAddress: string; // Recipient Sui address | ||
| amount: bigint; // Amount in smallest unit | ||
| coinType: string; // Coin type (e.g., '0x2::sui::SUI') | ||
| nonce: string; // Unique identifier (max 36 chars) | ||
| label?: string; // Optional human-readable label | ||
| message?: string; // Optional message/memo | ||
| iconUrl?: string; // Optional icon URL | ||
| } & Partial<Registry>; | ||
| ``` | ||
| **Returns:** `string` - The constructed payment URI | ||
| **Example:** | ||
| ```ts | ||
| // Create a URI for a registry-based payment | ||
| const uri = createPaymentTransactionUri({ | ||
| receiverAddress: '0x1234...abcd', | ||
| amount: 1000000000n, // 1 SUI | ||
| coinType: '0x2::sui::SUI', | ||
| nonce: crypto.randomUUID(), | ||
| registryName: 'my-payment-registry', | ||
| label: 'Coffee Purchase', | ||
| message: 'Thanks for your order!', | ||
| }); | ||
| // Result: | ||
| // sui:pay? | ||
| // receiver=0x1234... | ||
| // &amount=1000000000 | ||
| // &coinType=0x2::sui::SUI | ||
| // &nonce=abc-123-def | ||
| // ®istry=my-payment-registry | ||
| // &label=Coffee+Purchase | ||
| // &message=Thanks+for+your+order! | ||
| // Create a URI for an ephemeral payment (no registry) | ||
| const ephemeralUri = createPaymentTransactionUri({ | ||
| receiverAddress: '0x1234...abcd', | ||
| amount: 500000000n, // 0.5 SUI | ||
| coinType: '0x2::sui::SUI', | ||
| nonce: crypto.randomUUID(), | ||
| }); | ||
| ``` | ||
| **Throws:** | ||
| - `PaymentKitUriError` if receiver address is invalid | ||
| - `PaymentKitUriError` if amount is not positive | ||
| - `PaymentKitUriError` if coin type is invalid | ||
| - `PaymentKitUriError` if nonce is missing or exceeds 36 characters | ||
| - `PaymentKitUriError` if registry ID is not a valid Sui object ID | ||
| --- | ||
| ### `parsePaymentTransactionUri()` | ||
| Parses a payment transaction URI into its component parameters. | ||
| ```ts | ||
| parsePaymentTransactionUri(uri: string): PaymentUriParams | ||
| ``` | ||
| **Parameters:** | ||
| | Parameter | Type | Description | | ||
| | --------- | -------- | ------------------------ | | ||
| | `uri` | `string` | The payment URI to parse | | ||
| **Returns:** `PaymentUriParams` - The parsed payment parameters | ||
| **Example:** | ||
| ```ts | ||
| const uri = | ||
| 'sui:pay?receiver=0x1234...&amount=1000000000&coinType=0x2::sui::SUI&nonce=abc123®istry=my-registry'; | ||
| const params = parsePaymentTransactionUri(uri); | ||
| console.log(params.receiverAddress); // '0x1234...' | ||
| console.log(params.amount); // 1000000000n (bigint) | ||
| console.log(params.coinType); // '0x2::sui::SUI' | ||
| console.log(params.nonce); // 'abc123' | ||
| console.log(params.registryName); // 'my-registry' | ||
| // Use parsed params to process a payment | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| receiver: params.receiverAddress, | ||
| amount: params.amount, | ||
| coinType: params.coinType, | ||
| nonce: params.nonce, | ||
| registryName: params.registryName!, | ||
| sender: senderAddress, | ||
| }); | ||
| ``` | ||
| **Throws:** | ||
| - `PaymentKitUriError` if URI doesn't start with `sui:pay?` | ||
| - `PaymentKitUriError` if required parameters are missing | ||
| - `PaymentKitUriError` if receiver address is invalid | ||
| - `PaymentKitUriError` if coin type is invalid | ||
| - `PaymentKitUriError` if nonce exceeds 36 characters | ||
| - `PaymentKitUriError` if amount is not a positive number | ||
| ### Future versions | ||
| Check the | ||
| [CHANGELOG](https://github.com/MystenLabs/ts-sdks/blob/main/packages/payment-kit/CHANGELOG.md) for | ||
| updates. |
| # Payment Processing | ||
| > Process registry-based and ephemeral payments and handle transaction results on Sui. | ||
| > **Note:** This package is in active development and should be used with caution. APIs are experimental and | ||
| > subject to breaking changes without notice. We recommend thoroughly testing any implementation | ||
| > before using in production environments. | ||
| Payment Kit provides two distinct payment processing models, each designed for different use cases. | ||
| This guide explains how each model works and when to use them. | ||
| ## Payment models overview | ||
| ### Registry-based payments | ||
| Registry-based payments create a persistent `PaymentRecord` stored onchain in a `PaymentRegistry`. | ||
| This model provides: | ||
| - **Duplicate Prevention**: A payment with the same parameters can only be processed once | ||
| - **Verifiable Proof**: Onchain proof that a payment was made | ||
| - **Expiration Management**: Records can be deleted after a configurable period | ||
| - **Centralized Tracking**: All payments are indexed under a specific registry | ||
| **When to Use:** | ||
| - E-commerce checkouts | ||
| - Subscription payments | ||
| - Invoice payments | ||
| - Any scenario requiring duplicate prevention | ||
| ### Ephemeral payments | ||
| Ephemeral payments process transfers without creating persistent onchain records. This model | ||
| provides: | ||
| - **Lower Gas Costs**: No onchain storage means cheaper transactions | ||
| - **Flexibility**: No registry configuration required | ||
| **When to Use:** | ||
| - Tipping/donations | ||
| - Recurring payments with external tracking | ||
| - High-frequency microtransactions | ||
| - Scenarios where duplicate prevention is handled offchain | ||
| ## How payment keys work | ||
| Payment Kit uses a composite key system to uniquely identify payments. Understanding this is crucial | ||
| for using the SDK effectively. | ||
| ### Payment key components | ||
| A payment key is derived from four parameters: | ||
| ```ts | ||
| type PaymentKeyArgs = { | ||
| nonce: string; // Unique identifier you provide | ||
| amount: number; // Payment amount in smallest unit | ||
| receiver: string; // Recipient address | ||
| coinType: string; // Coin type being transferred | ||
| }; | ||
| ``` | ||
| ### Key generation | ||
| When you process a registry payment, Payment Kit hashes these parameters to create a unique key: | ||
| ```ts | ||
| // These parameters create a unique payment key | ||
| const paymentParams = { | ||
| nonce: 'b5e88aec-d88e-4961-9204-6c84e0e1de4e', | ||
| amount: 1000000000, | ||
| receiver, | ||
| coinType: '0x2::sui::SUI', | ||
| }; | ||
| // If you try to process the same payment twice, it will fail | ||
| const tx1 = client.paymentKit.tx.processRegistryPayment({ | ||
| ...paymentParams, | ||
| sender: senderAddress, | ||
| registryName: 'my-registry', | ||
| }); | ||
| // This will fail - same payment key | ||
| const tx2 = client.paymentKit.tx.processRegistryPayment({ | ||
| ...paymentParams, | ||
| sender: senderAddress, | ||
| registryName: 'my-registry', | ||
| }); | ||
| ``` | ||
| ### Changing any component creates a new key | ||
| Each component affects the payment key. Changing any parameter creates a different payment. This | ||
| means, as an example, that a `nonce` can be reused for a different payment, if the `coinType` or | ||
| `receiver` differ. | ||
| ```ts | ||
| // Original payment | ||
| const originalNonce = crypto.randomUUID(); | ||
| const payment1 = { | ||
| nonce: originalNonce, | ||
| amount: 1000000000, | ||
| receiver, | ||
| coinType: '0x2::sui::SUI', | ||
| }; | ||
| // Different nonce = different payment | ||
| const payment2 = { | ||
| nonce: crypto.randomUUID(), // Changed | ||
| amount: 1000000000, | ||
| receiver, | ||
| coinType: '0x2::sui::SUI', | ||
| }; | ||
| // Different amount = different payment | ||
| const payment3 = { | ||
| nonce: originalNonce, | ||
| amount: 2000000000, // Changed | ||
| receiver, | ||
| coinType: '0x2::sui::SUI', | ||
| }; | ||
| // All three are unique payments that can be processed separately | ||
| ``` | ||
| ## Processing registry-based payments | ||
| Let's walk through a complete registry payment workflow: | ||
| ### Step 1: Create the transaction | ||
| ```ts | ||
| // Define your payment parameters | ||
| const paymentParams = { | ||
| nonce: crypto.randomUUID(), // Your unique payment ID | ||
| coinType: '0x2::sui::SUI', // SUI token | ||
| amount: 1000000000, // 1 SUI (in MIST) | ||
| receiver, | ||
| sender: senderAddress, // Must match signer | ||
| }; | ||
| // Create the transaction | ||
| const tx = client.paymentKit.tx.processRegistryPayment(paymentParams); | ||
| ``` | ||
| ### What happens onchain | ||
| When this transaction executes, Payment Kit: | ||
| 1. **Validates** the payment parameters | ||
| 2. **Checks** if a payment with this key already exists in the registry | ||
| 3. **Transfers** the coins from sender to receiver | ||
| 4. **Creates** a `PaymentRecord` dynamic field on the registry | ||
| 5. **Emits** a `PaymentReceipt` event | ||
| ### Step 2: Execute the transaction | ||
| ```ts | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEffects: true, | ||
| showEvents: true, | ||
| showObjectChanges: true, | ||
| }, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Transaction digest:', result.Transaction.digest); | ||
| ``` | ||
| ### Step 3: Extract the receipt | ||
| ```ts | ||
| // Find the PaymentReceipt event | ||
| const receiptEvent = result.Transaction.events?.find((event) => | ||
| event.type.includes('PaymentReceipt'), | ||
| ); | ||
| if (receiptEvent) { | ||
| const receipt = receiptEvent.parsedJson; | ||
| console.log('Payment Receipt:'); | ||
| console.log(' Type:', receipt.payment_type); // 'Registry' | ||
| console.log(' Nonce:', receipt.nonce); // 'crypto.randomUUID()' | ||
| console.log(' Amount:', receipt.amount); // 1n * MIST_PER_SUI | ||
| console.log(' Receiver:', receipt.receiver); // | ||
| console.log(' Coin Type:', receipt.coin_type); // '0x2::sui::SUI' | ||
| console.log(' Timestamp:', receipt.timestamp_ms); // Unix timestamp | ||
| } | ||
| ``` | ||
| ### Step 4: Verify the payment record | ||
| ```ts | ||
| // Query the payment record to confirm it exists | ||
| const record = await client.paymentKit.getPaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| }); | ||
| if (record) { | ||
| console.log('Payment verified!'); | ||
| console.log('Record ID:', record.key); | ||
| console.log('Transaction:', record.paymentTransactionDigest); | ||
| console.log('Epoch:', record.epochAtTimeOfRecord); | ||
| } | ||
| ``` | ||
| ## Processing ephemeral payments | ||
| Ephemeral payments follow a simpler flow without creating persistent records: | ||
| ### Creating an ephemeral payment | ||
| ```ts | ||
| // No registry needed for ephemeral payments | ||
| const tx = client.paymentKit.tx.processEphemeralPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 500000000, // 0.5 SUI | ||
| receiver, | ||
| sender: senderAddress, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEvents: true, | ||
| }, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Ephemeral payment failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| ``` | ||
| ### What happens onchain | ||
| For ephemeral payments, Payment Kit: | ||
| 1. **Validates** the payment parameters | ||
| 2. **Transfers** the coins from sender to receiver | ||
| 3. **Emits** a `PaymentReceipt` event | ||
| No `PaymentRecord` is created, so duplicate payments are not prevented. | ||
| ### Extracting the receipt | ||
| ```ts | ||
| const receiptEvent = result.Transaction.events?.find((event) => | ||
| event.type.includes('PaymentReceipt'), | ||
| ); | ||
| if (receiptEvent) { | ||
| const receipt = receiptEvent.parsedJson; | ||
| console.log('Payment Type:', receipt.payment_type); // 'Ephemeral' | ||
| } | ||
| ``` | ||
| ## Advanced payment scenarios | ||
| ### Processing multiple payments in one transaction | ||
| You can combine multiple payment operations in a single transaction: | ||
| ```ts | ||
| const tx = new Transaction(); | ||
| // Add multiple registry payments | ||
| tx.add( | ||
| client.paymentKit.calls.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }), | ||
| ); | ||
| tx.add( | ||
| client.paymentKit.calls.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 2000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }), | ||
| ); | ||
| // Execute all payments in one transaction | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ### Working with custom coin types | ||
| Payment Kit works with any Sui coin type: | ||
| ```ts | ||
| // Example with a custom token | ||
| const CUSTOM_COIN = '0xabc123::my_token::MY_TOKEN'; | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: CUSTOM_COIN, | ||
| amount: 1000, // Amount in the coin's smallest unit | ||
| receiver, | ||
| sender: senderAddress, | ||
| }); | ||
| // Make sure the sender has enough of the custom coin | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ## Gas considerations | ||
| ### Registry payments | ||
| Registry payments have higher gas costs because they: | ||
| - Create a new dynamic field (first payment with unique key) | ||
| - Write data to the registry object | ||
| - Perform duplicate checking | ||
| ### Ephemeral payments | ||
| Ephemeral payments have lower gas costs because they: | ||
| - Only transfer coins | ||
| - Don't create persistent storage | ||
| - Skip duplicate checking | ||
| ## Best practices | ||
| 1. **Use Meaningful Nonces**: Choose nonces that help you track payments (for example, `order-123`, | ||
| `invoice-456`) | ||
| 2. **Store Receipts Off-Chain**: Save `PaymentReceipt` events in your database for quick lookup | ||
| 3. **Handle Duplicate Attempts**: Gracefully handle duplicate payment attempts with proper error | ||
| messages | ||
| 4. **Verify Before Delivery**: Always verify payment records before delivering goods or services | ||
| 5. **Choose the Right Model**: | ||
| - Use registry payments for critical transactions requiring duplicate prevention | ||
| - Use ephemeral payments for high-frequency, low-value transactions | ||
| 6. **Monitor Expiration**: If using registry payments, be aware of the expiration policy and query | ||
| records before they expire | ||
| ## Next steps | ||
| - [Registry Management](./registry-management) - Learn how to create and configure custom registries |
| # Registry Management | ||
| > Create, configure, and manage payment registries and accepted coin types on Sui. | ||
| > **Note:** This package is in active development and should be used with caution. APIs are experimental and | ||
| > subject to breaking changes without notice. We recommend thoroughly testing any implementation | ||
| > before using in production environments. | ||
| Payment registries are the core of Payment Kit's duplicate prevention and payment tracking system. | ||
| This guide covers creating, configuring, and managing payment registries. | ||
| ## Understanding payment registries | ||
| A `PaymentRegistry` is an onchain object that: | ||
| - Stores `PaymentRecord` dynamic fields for each unique payment | ||
| - Manages configuration settings (expiration, fund management) | ||
| - Can be owned and administered by a specific account | ||
| - Provides namespaced payment tracking | ||
| ### Default registry | ||
| Payment Kit provides a default registry that's ready to use: | ||
| ```ts | ||
| // Use the default registry | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| }); | ||
| ``` | ||
| ### When to create a custom registry | ||
| Consider creating your own registry when: | ||
| - You want isolated payment tracking for your application | ||
| - You need custom expiration policies | ||
| - You want to manage funds centrally | ||
| - You need better indexing of your payments | ||
| - You want to avoid potential congestion on the default registry | ||
| ## Creating a registry | ||
| ### Basic registry creation | ||
| Creating a registry is straightforward. You only need to provide a unique name: | ||
| ```ts | ||
| // Create a new registry | ||
| const tx = client.paymentKit.tx.createRegistry({ | ||
| registryName: 'my-app-payments', | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEffects: true, | ||
| showObjectChanges: true, | ||
| }, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Registry creation failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Registry created!'); | ||
| console.log('Transaction:', result.Transaction.digest); | ||
| ``` | ||
| ### What gets created | ||
| When you create a registry, two objects are created: | ||
| 1. **PaymentRegistry**: The registry object that stores payment records | ||
| 2. **RegistryAdminCap**: A capability object that grants admin permissions | ||
| ### Extracting the admin capability | ||
| The `RegistryAdminCap` is crucial for configuring your registry: | ||
| ```ts | ||
| // Find the admin cap in the transaction result | ||
| const adminCapObject = result.Transaction.objectChanges?.find( | ||
| (change) => change.type === 'created' && change.objectType.includes('RegistryAdminCap'), | ||
| ); | ||
| if (adminCapObject && 'objectId' in adminCapObject) { | ||
| const adminCapId = adminCapObject.objectId; | ||
| console.log('Admin Capability ID:', adminCapId); | ||
| // Store this ID - you'll need it to configure the registry | ||
| await database.saveAdminCap({ | ||
| registryName: 'my-app-payments', | ||
| adminCapId: adminCapId, | ||
| owner: senderAddress, | ||
| }); | ||
| } | ||
| ``` | ||
| ### Deriving the registry ID | ||
| Payment Kit derives registry IDs deterministically from the registry name: | ||
| ```ts | ||
| // You can compute the registry ID without querying the chain | ||
| const registryName = 'random-registry-name'; | ||
| const registryId = client.paymentKit.getRegistryIdFromName(registryName); | ||
| console.log('Registry ID:', registryId); | ||
| ``` | ||
| This allows you to reference registries by name throughout your application. | ||
| ## Registry configuration | ||
| Registries have two main configuration options: | ||
| ### Step 1: Epoch expiration duration | ||
| Controls how long payment records persist before they can be deleted. | ||
| **Default**: 30 epochs (~30 days on Mainnet) | ||
| ```ts | ||
| // Set expiration to 60 epochs | ||
| const tx = client.paymentKit.tx.setConfigEpochExpirationDuration({ | ||
| registryName: 'my-app-payments', | ||
| epochExpirationDuration: 60, | ||
| adminCapId: adminCapId, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (result.$kind === 'FailedTransaction') { | ||
| throw new Error(`Configuration update failed: ${result.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Expiration set to 60 epochs'); | ||
| ``` | ||
| **How It Works:** | ||
| When a payment is recorded, it stores the current epoch. After the expiration duration has passed, | ||
| anyone can delete the record to reclaim storage fees: | ||
| ```ts | ||
| // Current epoch: 1000 | ||
| // Payment recorded at epoch: 1000 | ||
| // Expiration duration: 30 | ||
| // Can be deleted after epoch: 1030 | ||
| ``` | ||
| **Use Cases:** | ||
| - **Short expiration (7-14 epochs)**: High-volume, time-sensitive payments (subscriptions, tickets) | ||
| - **Medium expiration (30-60 epochs)**: Standard e-commerce transactions | ||
| - **Long expiration (180+ epochs)**: Important financial records requiring long-term verification | ||
| ### Step 2: Registry managed funds | ||
| Controls whether payments must be sent to the registry itself for later withdrawal. | ||
| **Default**: Disabled (funds go directly to receivers) | ||
| ```ts | ||
| // Enable registry-managed funds | ||
| const tx = client.paymentKit.tx.setConfigRegistryManagedFunds({ | ||
| registryName: 'my-app-payments', | ||
| registryManagedFunds: true, | ||
| adminCapId: adminCapId, | ||
| }); | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| console.log('Registry now manages funds'); | ||
| ``` | ||
| **How it works:** | ||
| When enabled: | ||
| - All payments must specify the registry as the receiver | ||
| - Coins are transferred to the registry object | ||
| - Registry admin can withdraw accumulated funds later | ||
| - Simplifies coin merging for high-throughput scenarios | ||
| ```ts | ||
| // With registry-managed funds enabled | ||
| const registryId = getRegistryIdFromName('my-app-payments', namespaceId); | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver: registryId, // Must be the registry itself | ||
| sender: senderAddress, | ||
| registryName: 'my-app-payments', | ||
| }); | ||
| ``` | ||
| **Use Cases:** | ||
| - Platforms collecting payments on behalf of sellers | ||
| - Applications that need to batch process payouts | ||
| - Scenarios with very high transaction volumes | ||
| - Simplified accounting and reconciliation | ||
| ### Configuring both settings | ||
| You can configure both settings independently: | ||
| ```ts | ||
| const tx = new Transaction(); | ||
| // Set expiration duration | ||
| tx.add( | ||
| client.paymentKit.calls.setConfigEpochExpirationDuration({ | ||
| registryName: 'my-app-payments', | ||
| epochExpirationDuration: 90, | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| // Enable managed funds | ||
| tx.add( | ||
| client.paymentKit.calls.setConfigRegistryManagedFunds({ | ||
| registryName: 'my-app-payments', | ||
| registryManagedFunds: true, | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| // Apply both configurations in one transaction | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ## Withdrawing funds from a registry | ||
| If you've enabled registry-managed funds, you can withdraw accumulated coins: | ||
| ```ts | ||
| // Withdraw all SUI from the registry | ||
| const tx = client.paymentKit.tx.withdrawFromRegistry({ | ||
| coinType: '0x2::sui::SUI', | ||
| registryName: 'my-app-payments', | ||
| adminCapId: adminCapId, | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| options: { | ||
| showEffects: true, | ||
| }, | ||
| }); | ||
| console.log('Funds withdrawn!'); | ||
| ``` | ||
| **Important notes:** | ||
| - Only the admin cap owner can withdraw funds | ||
| - You must specify the coin type to withdraw | ||
| - All coins of that type are withdrawn in one operation | ||
| - The withdrawn coins are sent to the transaction sender | ||
| ### Withdrawing multiple coin types | ||
| If your registry accumulates different coin types: | ||
| ```ts | ||
| const tx = new Transaction(); | ||
| // Withdraw SUI | ||
| tx.add( | ||
| client.paymentKit.calls.withdrawFromRegistry({ | ||
| coinType: '0x2::sui::SUI', | ||
| registryName: 'my-app-payments', | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| // Withdraw custom token | ||
| tx.add( | ||
| client.paymentKit.calls.withdrawFromRegistry({ | ||
| coinType: '0xabc123::my_token::MY_TOKEN', | ||
| registryName: 'my-app-payments', | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| ``` | ||
| ## Deleting expired payment records | ||
| After the expiration period, payment records can be deleted to reclaim storage fees: | ||
| ```ts | ||
| // Delete an expired payment record | ||
| const tx = client.paymentKit.tx.deletePaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| registryName: 'my-app-payments', | ||
| }); | ||
| const result = await client.signAndExecuteTransaction({ | ||
| transaction: tx, | ||
| signer: keypair, | ||
| }); | ||
| console.log('Record deleted, storage rebate received'); | ||
| ``` | ||
| **Key points:** | ||
| - Records can only be deleted after expiration | ||
| - Anyone can delete expired records (permissionless cleanup) | ||
| - The deleter receives a small storage rebate | ||
| - This incentivizes automatic cleanup of old records | ||
| ### Checking if a record can be deleted | ||
| ```ts | ||
| // Get the payment record | ||
| const record = await client.paymentKit.getPaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| registryName: 'my-app-payments', | ||
| }); | ||
| if (record) { | ||
| const recordEpoch = parseInt(record.epochAtTimeOfRecord); | ||
| const currentEpoch = await client.core | ||
| .getLatestSuiSystemState() | ||
| .then((state) => Number(state.epoch)); | ||
| const expirationDuration = 30; // Your registry's setting | ||
| const canDelete = currentEpoch >= recordEpoch + expirationDuration; | ||
| console.log('Record epoch:', recordEpoch); | ||
| console.log('Current epoch:', currentEpoch); | ||
| console.log('Can delete:', canDelete); | ||
| } | ||
| ``` | ||
| ## Using registry ID directly | ||
| If you know the registry ID, you can use it instead of the name: | ||
| ```ts | ||
| const registryId = '0x123abc...'; | ||
| // Process payment using registry ID | ||
| const tx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| sender: senderAddress, | ||
| registryId: registryId, // Use ID instead of name | ||
| }); | ||
| // Query using registry ID | ||
| const record = await client.paymentKit.getPaymentRecord({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver, | ||
| registryId: registryId, | ||
| }); | ||
| ``` | ||
| ## Complete registry setup example | ||
| Here's a complete workflow for setting up a custom registry: | ||
| ```ts | ||
| const client = new SuiGrpcClient({ | ||
| network: 'testnet', | ||
| baseUrl: 'https://fullnode.testnet.sui.io:443', | ||
| }).$extend(paymentKit()); | ||
| const keypair = Ed25519Keypair.generate(); | ||
| const registryName = 'my-marketplace-registry'; | ||
| // Step 1: Create the registry | ||
| console.log('Creating registry...'); | ||
| const createTx = client.paymentKit.tx.createRegistry({ | ||
| registryName: registryName, | ||
| }); | ||
| const createResult = await client.signAndExecuteTransaction({ | ||
| transaction: createTx, | ||
| signer: keypair, | ||
| options: { | ||
| showEffects: true, | ||
| showObjectChanges: true, | ||
| }, | ||
| }); | ||
| // Check transaction status | ||
| if (createResult.$kind === 'FailedTransaction') { | ||
| throw new Error( | ||
| `Registry creation failed: ${createResult.FailedTransaction.status.error?.message}`, | ||
| ); | ||
| } | ||
| // Step 2: Extract the admin cap | ||
| const adminCapObject = createResult.Transaction.objectChanges?.find( | ||
| (change) => change.type === 'created' && change.objectType.includes('RegistryAdminCap'), | ||
| ); | ||
| const adminCapId = adminCapObject && 'objectId' in adminCapObject ? adminCapObject.objectId : ''; | ||
| console.log('Registry created!'); | ||
| console.log('Admin Cap ID:', adminCapId); | ||
| // Step 3: Configure the registry | ||
| console.log('Configuring registry...'); | ||
| const configTx = new Transaction(); | ||
| // Set 60-epoch expiration | ||
| configTx.add( | ||
| client.paymentKit.calls.setConfigEpochExpirationDuration({ | ||
| registryName: registryName, | ||
| epochExpirationDuration: 60, | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| // Enable managed funds | ||
| configTx.add( | ||
| client.paymentKit.calls.setConfigRegistryManagedFunds({ | ||
| registryName: registryName, | ||
| registryManagedFunds: true, | ||
| adminCapId: adminCapId, | ||
| }), | ||
| ); | ||
| const configResult = await client.signAndExecuteTransaction({ | ||
| transaction: configTx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (configResult.$kind === 'FailedTransaction') { | ||
| throw new Error(`Configuration failed: ${configResult.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('Registry configured!'); | ||
| console.log('Ready to process payments'); | ||
| // Step 4: Process a payment | ||
| const registryId = getRegistryIdFromName(registryName, namespaceId); | ||
| const paymentTx = client.paymentKit.tx.processRegistryPayment({ | ||
| nonce: crypto.randomUUID(), | ||
| coinType: '0x2::sui::SUI', | ||
| amount: 1000000000, | ||
| receiver: registryId, // Funds go to registry | ||
| sender: keypair.getPublicKey().toSuiAddress(), | ||
| registryName: registryName, | ||
| }); | ||
| const paymentResult = await client.signAndExecuteTransaction({ | ||
| transaction: paymentTx, | ||
| signer: keypair, | ||
| }); | ||
| // Check transaction status | ||
| if (paymentResult.$kind === 'FailedTransaction') { | ||
| throw new Error(`Payment failed: ${paymentResult.FailedTransaction.status.error?.message}`); | ||
| } | ||
| console.log('First payment processed:', paymentResult.Transaction.digest); | ||
| ``` | ||
| ## Best practices | ||
| 1. **Store admin caps securely**: Treat your admin capability like private keys - they control your | ||
| registry | ||
| 2. **Use descriptive names**: Choose registry names that clearly identify their purpose (for | ||
| example, `acme-subscriptions`, `store-123-payments`) | ||
| 3. **Set appropriate expiration**: Balance storage costs with verification needs | ||
| - Short expiration = lower storage costs | ||
| - Long expiration = longer verification period | ||
| 4. **Monitor registry growth**: Track the number of payment records to anticipate storage costs | ||
| 5. **Plan fund management**: Decide upfront whether to use registry-managed funds based on your | ||
| withdrawal patterns | ||
| 6. **Document your configuration**: Keep records of your registry settings for operational | ||
| consistency | ||
| 7. **Test thoroughly**: Always test registry operations on Testnet before Mainnet deployment | ||
| ## Next steps | ||
| - [SDK API Reference](./payment-kit-sdk) - Complete SDK API documentation |
+3
-3
| { | ||
| "name": "@mysten/payment-kit", | ||
| "version": "0.1.10", | ||
| "version": "0.1.11", | ||
| "private": false, | ||
@@ -35,6 +35,6 @@ "description": "Sui Payment Kit", | ||
| "dependencies": { | ||
| "@mysten/bcs": "^2.0.4" | ||
| "@mysten/bcs": "^2.0.5" | ||
| }, | ||
| "peerDependencies": { | ||
| "@mysten/sui": "^2.16.1" | ||
| "@mysten/sui": "^2.16.2" | ||
| }, | ||
@@ -41,0 +41,0 @@ "scripts": { |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
160726
49.75%48
14.29%Updated