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
2 changes: 1 addition & 1 deletion packages/atxp-express/src/atxpExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
detected.credential,
context as Parameters<typeof settlement.settle>[2],
);
logger.info(`Settled ${detected.protocol} in middleware: txHash=${result.txHash}, amount=${result.settledAmount}`);
logger.info(`Settled ${detected.protocol} in middleware: txHash=${result.txHash ?? '<already-settled>'}, amount=${result.settledAmount}`);
} catch (error) {
logger.error(`Middleware settlement failed for ${detected.protocol}: ${error instanceof Error ? error.message : String(error)}`);
// Don't store the credential — it's already consumed/invalid.
Expand Down Expand Up @@ -223,11 +223,11 @@
res.writeHead = function writeHeadDeferred(this: Response, ...args: any[]): any {
deferredWriteHead = args;
return this;
} as any;

Check warning on line 226 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

function flushWriteHead(self: Response): void {
if (!deferredWriteHead) return;
(origWriteHead as any).apply(self, deferredWriteHead);

Check warning on line 230 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
deferredWriteHead = null;
}

Expand All @@ -236,8 +236,8 @@
res.write = function writeWithPaymentRewrite(this: Response, ...args: any[]): any {
flushWriteHead(this);
args[0] = rewriteChunk(args[0]);
return (origWrite as any).apply(this, args);

Check warning on line 239 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} as any;

Check warning on line 240 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

// Hook res.end for non-SSE (enableJsonResponse) responses.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -265,12 +265,12 @@
}
}
}
(origWriteHead as any).apply(this, deferredWriteHead);

Check warning on line 268 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
deferredWriteHead = null;
}

return (origEnd as any).apply(this, args);

Check warning on line 272 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} as any;

Check warning on line 273 in packages/atxp-express/src/atxpExpress.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
}

/**
Expand Down
25 changes: 25 additions & 0 deletions packages/atxp-server/src/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,31 @@ describe('ProtocolSettlement', () => {
await expect(settlement.settle('x402', 'cred')).rejects.toThrow('Settlement failed for x402: 500');
});

it('should pass through null txHash when auth reports already-settled', async () => {
// The auth server returns { txHash: null, alreadySettled: true, ... } on
// retries of an already-settled payload. The SDK should surface it as-is
// rather than crashing or forcing the type to string.
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({
txHash: null,
settledAmount: '201000',
alreadySettled: true,
network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
payer: '3FnrCCfHhZhEyeQd5Q69B1faqLvdHoG3WxUesZBBJ7M2',
sourceAccountId: 'atxp:atxp_acct_6qB245zVIJeSiIHi8xPmY',
}),
});

const payload = { signature: '0xabc' };
const credential = Buffer.from(JSON.stringify(payload)).toString('base64');
const result = await settlement.settle('x402', credential, { paymentRequirements: { network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' } });

expect(result.txHash).toBeNull();
expect(result.settledAmount).toBe('201000');
expect(result.alreadySettled).toBe(true);
});

describe('X402 multi-chain accept routing', () => {
const multiChainReqs = {
x402Version: 2,
Expand Down
10 changes: 8 additions & 2 deletions packages/atxp-server/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,16 @@ export type VerifyResult = {

/**
* Result of settling a payment.
*
* `txHash` is null when the auth server reports the payment was already settled
* by a prior call (HTTP retry after a successful settle). The original tx hash
* is not carried in that response; callers that need it must look it up by
* other means (payer / amount / destination).
*/
export type SettleResult = {
txHash: string;
txHash: string | null;
settledAmount: string;
alreadySettled?: boolean;
};

/**
Expand Down Expand Up @@ -230,7 +236,7 @@ export class ProtocolSettlement {
}

const result = await response.json() as SettleResult;
this.logger.info(`Settled ${protocol}: txHash=${result.txHash}, amount=${result.settledAmount}`);
this.logger.info(`Settled ${protocol}: txHash=${result.txHash ?? '<already-settled>'}, amount=${result.settledAmount}`);
return result;
}

Expand Down
Loading