mirror of
https://github.com/hanzoai/account-sdk.git
synced 2026-09-24 16:55:42 +00:00
Add dataSuffix to pay() with input validation (#254)
* Add dataSuffix support to pay() and attribution validation - Add optional `dataSuffix` field to `PaymentOptions` type (0x-prefixed hex) - Thread dataSuffix through pay() -> executePaymentWithSDK -> createEphemeralSDK - Set attribution.dataSuffix on SDK preference when dataSuffix is provided - Add validateDataSuffix() to both sdkManager and base-account utils - Remove fixed 16-byte length constraint from makeDataSuffix - Update README and payment README with dataSuffix usage examples - Add test coverage for dataSuffix passthrough and validation Made-with: Cursor * chore: apply formatter fix to pay.ts destructuring Made-with: Cursor * chore: remove 16-byte dataSuffix reference from testapp SDKConfig Made-with: Cursor * format * unify type to viem Hex
This commit is contained in:
@@ -37,6 +37,7 @@ import { pay } from '@base-org/account';
|
||||
const payment = await pay({
|
||||
amount: "10.50", // Amount in USDC
|
||||
to: "0xYourWalletAddress", // Your wallet address
|
||||
dataSuffix: "0xabc123", // Optional attribution data suffix
|
||||
testnet: true // Use testnet for testing
|
||||
});
|
||||
|
||||
|
||||
@@ -96,8 +96,8 @@ export function SDKConfig() {
|
||||
<Code ml={2}>attribution.dataSuffix</Code>
|
||||
</Flex>
|
||||
<Text mt={2} fontSize="sm">
|
||||
First 16 bytes of a unique string to identify your onchain activity. Update the text
|
||||
box below to have your data suffix applied
|
||||
A 0x-prefixed hex string to identify your onchain activity. Update the text box
|
||||
below to have your data suffix applied
|
||||
</Text>
|
||||
<FormControl mt={2}>
|
||||
<Input
|
||||
|
||||
@@ -59,7 +59,7 @@ export type Attribution =
|
||||
}
|
||||
| {
|
||||
auto?: never;
|
||||
dataSuffix: `0x${string}`;
|
||||
dataSuffix: Hex;
|
||||
};
|
||||
|
||||
export type Preference = {
|
||||
@@ -74,8 +74,7 @@ export type Preference = {
|
||||
* @type {Attribution}
|
||||
* @note Smart Wallet only
|
||||
* @description This option only applies to Coinbase Smart Wallet. When a valid data suffix is supplied, it is appended to the initCode and executeBatch calldata.
|
||||
* Coinbase Smart Wallet expects a 16 byte hex string. If the data suffix is not a 16 byte hex string, the Smart Wallet will ignore the property. If auto is true,
|
||||
* the Smart Wallet will generate a 16 byte hex string from the apps origin.
|
||||
* If auto is true, the Smart Wallet will generate a deterministic hex suffix from the app's origin.
|
||||
*/
|
||||
attribution?: Attribution;
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import { store } from ':store/store.js';
|
||||
import * as checkCrossOriginModule from ':util/checkCrossOriginOpenerPolicy.js';
|
||||
import * as validatePreferencesModule from ':util/validatePreferences.js';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Hex } from 'viem';
|
||||
import { BaseAccountProvider } from './BaseAccountProvider.js';
|
||||
import { CreateProviderOptions, createBaseAccountSDK } from './createBaseAccountSDK.js';
|
||||
import * as getInjectedProviderModule from './getInjectedProvider.js';
|
||||
@@ -491,7 +492,7 @@ describe('createProvider', () => {
|
||||
|
||||
it('should handle complex nested preference objects', () => {
|
||||
const complexPreference = {
|
||||
attribution: { dataSuffix: '0x1234567890123456' as `0x${string}` },
|
||||
attribution: { dataSuffix: '0x1234567890123456' as Hex },
|
||||
telemetry: false,
|
||||
customProperty: 'custom value',
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { pay } from '@base-org/account';
|
||||
const payment = await pay({
|
||||
amount: "10.50",
|
||||
to: "0xFe21034794A5a574B94fE4fDfD16e005F1C96e51",
|
||||
dataSuffix: "0xabc123",
|
||||
testnet: true
|
||||
});
|
||||
|
||||
@@ -119,6 +120,7 @@ const status = await getPaymentStatus({
|
||||
|
||||
- `amount: string` - Amount of USDC to send as a string (e.g., "10.50")
|
||||
- `to: string` - Ethereum address to send payment to
|
||||
- `dataSuffix?: Hex` - Optional attribution data suffix as 0x-prefixed hex (viem `Hex` type)
|
||||
- `testnet?: boolean` - Whether to use Base Sepolia testnet (default: false)
|
||||
- `payerInfo?: PayerInfo` - Optional payer information configuration for data callbacks
|
||||
- `telemetry?: boolean` - Whether to enable telemetry logging (default: true)
|
||||
|
||||
@@ -339,7 +339,8 @@ describe('pay', () => {
|
||||
expect.any(Object),
|
||||
false,
|
||||
undefined,
|
||||
false
|
||||
false,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
@@ -387,7 +388,8 @@ describe('pay', () => {
|
||||
}),
|
||||
true,
|
||||
undefined,
|
||||
true
|
||||
true,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
@@ -462,6 +464,33 @@ describe('pay', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass dataSuffix through to SDK manager', async () => {
|
||||
vi.mocked(validation.validateStringAmount).mockReturnValue(undefined);
|
||||
vi.mocked(translatePayment.translatePaymentToSendCalls).mockReturnValue({
|
||||
version: '2.0.0',
|
||||
chainId: 8453,
|
||||
calls: [],
|
||||
capabilities: {},
|
||||
});
|
||||
vi.mocked(sdkManager.executePaymentWithSDK).mockResolvedValue({
|
||||
transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
});
|
||||
|
||||
await pay({
|
||||
amount: '10.50',
|
||||
to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51',
|
||||
dataSuffix: '0xabc123',
|
||||
});
|
||||
|
||||
expect(sdkManager.executePaymentWithSDK).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
false,
|
||||
undefined,
|
||||
true,
|
||||
'0xabc123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle different error types and normalize them', async () => {
|
||||
// Test with string error
|
||||
vi.mocked(validation.validateStringAmount).mockImplementation(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { normalizeAddress, validateStringAmount } from './utils/validation.js';
|
||||
* @param options - Payment options
|
||||
* @param options.amount - Amount of USDC to send as a string (e.g., "10.50")
|
||||
* @param options.to - Ethereum address to send payment to
|
||||
* @param options.dataSuffix - Optional 0x-prefixed hex attribution suffix
|
||||
* @param options.testnet - Whether to use Base Sepolia testnet (default: false)
|
||||
* @param options.payerInfo - Optional payer information configuration for data callbacks
|
||||
* @returns Promise<PaymentResult> - Result of the payment transaction
|
||||
@@ -35,7 +36,15 @@ import { normalizeAddress, validateStringAmount } from './utils/validation.js';
|
||||
* ```
|
||||
*/
|
||||
export async function pay(options: PaymentOptions): Promise<PaymentResult> {
|
||||
const { amount, to, testnet = false, payerInfo, walletUrl, telemetry = true } = options;
|
||||
const {
|
||||
amount,
|
||||
to,
|
||||
dataSuffix,
|
||||
testnet = false,
|
||||
payerInfo,
|
||||
walletUrl,
|
||||
telemetry = true,
|
||||
} = options;
|
||||
|
||||
// Generate correlation ID for this payment request
|
||||
const correlationId = crypto.randomUUID();
|
||||
@@ -62,7 +71,8 @@ export async function pay(options: PaymentOptions): Promise<PaymentResult> {
|
||||
requestParams,
|
||||
testnet,
|
||||
walletUrl,
|
||||
telemetry
|
||||
telemetry,
|
||||
dataSuffix
|
||||
);
|
||||
|
||||
// Log payment completed
|
||||
|
||||
@@ -61,6 +61,11 @@ export interface PaymentOptions {
|
||||
amount: string;
|
||||
/** Ethereum address to send payment to */
|
||||
to: string;
|
||||
/**
|
||||
* Optional attribution data suffix to append to wallet call data.
|
||||
* Must be a 0x-prefixed hex string (e.g., "0xabc123").
|
||||
*/
|
||||
dataSuffix?: Hex;
|
||||
/** Whether to use testnet (Base Sepolia). Defaults to false (mainnet) */
|
||||
testnet?: boolean;
|
||||
/** Optional payer information configuration for data callbacks */
|
||||
|
||||
@@ -41,9 +41,15 @@ export interface PaymentExecutionResult {
|
||||
* @param chainId - The chain ID to use
|
||||
* @param walletUrl - Optional wallet URL to use
|
||||
* @param telemetry - Whether to enable telemetry (defaults to true)
|
||||
* @param dataSuffix - Optional attribution data suffix
|
||||
* @returns The configured SDK instance
|
||||
*/
|
||||
export function createEphemeralSDK(chainId: number, walletUrl?: string, telemetry: boolean = true) {
|
||||
export function createEphemeralSDK(
|
||||
chainId: number,
|
||||
walletUrl?: string,
|
||||
telemetry: boolean = true,
|
||||
dataSuffix?: Hex
|
||||
) {
|
||||
const appName = typeof window !== 'undefined' ? window.location.origin : 'Base Pay SDK';
|
||||
|
||||
const sdk = createBaseAccountSDK({
|
||||
@@ -52,6 +58,7 @@ export function createEphemeralSDK(chainId: number, walletUrl?: string, telemetr
|
||||
preference: {
|
||||
telemetry: telemetry,
|
||||
walletUrl,
|
||||
attribution: dataSuffix ? { dataSuffix } : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -118,18 +125,20 @@ export async function executePayment(
|
||||
* @param testnet - Whether to use testnet
|
||||
* @param walletUrl - Optional wallet URL to use
|
||||
* @param telemetry - Whether to enable telemetry (defaults to true)
|
||||
* @param dataSuffix - Optional attribution data suffix
|
||||
* @returns The payment execution result
|
||||
*/
|
||||
export async function executePaymentWithSDK(
|
||||
requestParams: WalletSendCallsRequestParams,
|
||||
testnet: boolean,
|
||||
walletUrl?: string,
|
||||
telemetry: boolean = true
|
||||
telemetry: boolean = true,
|
||||
dataSuffix?: Hex
|
||||
): Promise<PaymentExecutionResult> {
|
||||
const network = testnet ? 'baseSepolia' : 'base';
|
||||
const chainId = CHAIN_IDS[network];
|
||||
|
||||
const sdk = createEphemeralSDK(chainId, walletUrl, telemetry);
|
||||
const sdk = createEphemeralSDK(chainId, walletUrl, telemetry, dataSuffix);
|
||||
const provider = sdk.getProvider();
|
||||
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
initSubAccountConfig,
|
||||
injectRequestCapabilities,
|
||||
isSendCallsParams,
|
||||
makeDataSuffix,
|
||||
prependWithoutDuplicates,
|
||||
requestHasCapability,
|
||||
} from './utils.js';
|
||||
@@ -57,6 +58,35 @@ describe('utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeDataSuffix', () => {
|
||||
it('throws for suffixes without a 0x prefix', () => {
|
||||
expect(() =>
|
||||
makeDataSuffix({
|
||||
attribution: { dataSuffix: 'abc123' as any },
|
||||
dappOrigin: 'https://example.com',
|
||||
})
|
||||
).toThrow('Invalid dataSuffix: expected a 0x-prefixed hex string');
|
||||
});
|
||||
|
||||
it('accepts suffixes with a 0x prefix', () => {
|
||||
expect(
|
||||
makeDataSuffix({
|
||||
attribution: { dataSuffix: '0xabc123' },
|
||||
dappOrigin: 'https://example.com',
|
||||
})
|
||||
).toBe('0xabc123');
|
||||
});
|
||||
|
||||
it('does not enforce a fixed 16-byte suffix length', () => {
|
||||
expect(
|
||||
makeDataSuffix({
|
||||
attribution: { dataSuffix: '0x1234567890abcdef1234567890abcdef1234567890abcdef' },
|
||||
dappOrigin: 'https://example.com',
|
||||
})
|
||||
).toBe('0x1234567890abcdef1234567890abcdef1234567890abcdef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertParamsChainId', () => {
|
||||
it('should throw if the params are not an array', () => {
|
||||
expect(() => assertParamsChainId({})).toThrow();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PublicClient, WalletSendCallsParameters, hexToBigInt, isAddress } from 'viem';
|
||||
|
||||
import { InsufficientBalanceErrorData } from ':core/error/errors.js';
|
||||
import { Hex, keccak256, numberToHex, slice, toHex } from 'viem';
|
||||
import { Hex, isHex, keccak256, numberToHex, slice, toHex } from 'viem';
|
||||
|
||||
import { standardErrors } from ':core/error/errors.js';
|
||||
import { Attribution, RequestArguments } from ':core/provider/interface.js';
|
||||
@@ -523,6 +523,13 @@ export function compute16ByteHash(input: string): Hex {
|
||||
return slice(keccak256(toHex(input)), 0, 16);
|
||||
}
|
||||
|
||||
export function validateDataSuffix(dataSuffix: string): Hex {
|
||||
if (!isHex(dataSuffix)) {
|
||||
throw new Error('Invalid dataSuffix: expected a 0x-prefixed hex string');
|
||||
}
|
||||
return dataSuffix;
|
||||
}
|
||||
|
||||
export function makeDataSuffix({
|
||||
attribution,
|
||||
dappOrigin,
|
||||
@@ -535,8 +542,8 @@ export function makeDataSuffix({
|
||||
return compute16ByteHash(dappOrigin);
|
||||
}
|
||||
|
||||
if ('dataSuffix' in attribution) {
|
||||
return attribution.dataSuffix;
|
||||
if ('dataSuffix' in attribution && typeof attribution.dataSuffix === 'string') {
|
||||
return validateDataSuffix(attribution.dataSuffix);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user