Skip to content
Merged
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
6 changes: 5 additions & 1 deletion env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
NODE_ENV=development
ATXP_AUTH_CLIENT_TOKEN=

# For dev scripts
SOLANA_ENDPOINT=
SOLANA_PRIVATE_KEY=
ATXP_DESTINATION=
ATXP_AUTH_CLIENT_TOKEN=
ATXP_CONNECTION_STRING=
BASE_RPC=
BASE_PRIVATE_KEY=

180 changes: 176 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/atxp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"bignumber.js": "^9.3.0",
"bs58": "^6.0.0",
"oauth4webapi": "^3.5.0",
"react-native-url-polyfill": "^2.0.0"
"react-native-url-polyfill": "^2.0.0",
"viem": "^2.34.0"
},
"peerDependencies": {
"expo-crypto": ">=14.0.0",
Expand Down
24 changes: 24 additions & 0 deletions packages/atxp-client/src/baseAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Account, PaymentMaker } from './types.js';
import { privateKeyToAccount } from 'viem/accounts';
import { BasePaymentMaker } from './basePaymentMaker.js';

export class BaseAccount implements Account {
accountId: string;
paymentMakers: { [key: string]: PaymentMaker };

constructor(baseRPCUrl: string, sourceSecretKey: `0x${string}`) {
if (!baseRPCUrl) {
throw new Error('Base RPC URL is required');
}
if (!sourceSecretKey) {
throw new Error('Source secret key is required');
}

const account = privateKeyToAccount(sourceSecretKey);

this.accountId = account.address;
this.paymentMakers = {
'base': new BasePaymentMaker(baseRPCUrl, sourceSecretKey),
}
}
}
62 changes: 62 additions & 0 deletions packages/atxp-client/src/basePaymentMaker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { BasePaymentMaker } from './basePaymentMaker.js';

describe('basePaymentMaker.generateJWT', () => {
it('should generate a valid JWT with default payload', async () => {
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const paymentMaker = new BasePaymentMaker('https://example.com', privateKey);
const jwt = await paymentMaker.generateJWT({paymentRequestId: '', codeChallenge: 'testCodeChallenge'});

// JWT format: header.payload.signature (all base64url)
const [headerB64, payloadB64, signatureB64] = jwt.split('.');
expect(headerB64).toBeDefined();
expect(payloadB64).toBeDefined();
expect(signatureB64).toBeDefined();

// Decode header and payload
const decodeB64Url = (str: string) => {
// Pad string for base64 decoding
let b64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
return JSON.parse(Buffer.from(b64, 'base64').toString('utf-8'));
};
const header = decodeB64Url(headerB64);
const payload = decodeB64Url(payloadB64);

expect(header.alg).toBe('ES256K');
expect(header.typ).toBeUndefined(); // BasePaymentMaker doesn't set typ
expect(payload.sub).toBe(account.address);
expect(payload.iss).toBe('accounts.atxp.ai');
expect(payload.aud).toBe('https://auth.atxp.ai');
expect(typeof payload.iat).toBe('number');
expect(payload.paymentIds).toBeUndefined();

// Signature verification would require ES256K (secp256k1) verification
// which is different from the EdDSA verification used in SolanaPaymentMaker
// For now, we just verify the signature is present and properly formatted
expect(signatureB64).toBeDefined();
expect(signatureB64.length).toBeGreaterThan(0);

// Decode the signature to verify it's a hex string with 0x prefix
const decodedSig = Buffer.from(signatureB64, 'base64url').toString('utf8');
expect(decodedSig).toMatch(/^0x[a-fA-F0-9]+$/);
});

it('should include payment request id if provided', async () => {
const privateKey = generatePrivateKey();
const paymentMaker = new BasePaymentMaker('https://example.com', privateKey);
const paymentRequestId = 'id1';
const jwt = await paymentMaker.generateJWT({paymentRequestId, codeChallenge: ''});
const [, payloadB64] = jwt.split('.');
const decodeB64Url = (str: string) => {
let b64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
return JSON.parse(Buffer.from(b64, 'base64').toString('utf-8'));
};
const payload = decodeB64Url(payloadB64);
expect(payload.payment_request_id).toEqual(paymentRequestId);
});
});

Loading