Skip to content

Commit f8540b6

Browse files
authored
Widen signer types (coinbase#502)
1 parent 6b4da81 commit f8540b6

File tree

16 files changed

+165
-61
lines changed

16 files changed

+165
-61
lines changed

typescript/packages/x402/src/client/createPaymentHeader.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeAll } from "vitest";
2-
import { generateKeyPairSigner, type KeyPairSigner } from "@solana/kit";
2+
import { generateKeyPairSigner, type TransactionSigner } from "@solana/kit";
33
import { createPaymentHeader } from "./createPaymentHeader";
44
import { PaymentRequirements } from "../types/verify";
55
import * as exactSvmClient from "../schemes/exact/svm/client";
@@ -9,7 +9,7 @@ vi.mock("../schemes/exact/svm/client", () => ({
99
}));
1010

1111
describe("createPaymentHeader", () => {
12-
let svmSigner: KeyPairSigner;
12+
let svmSigner: TransactionSigner;
1313
let paymentRequirements: PaymentRequirements;
1414

1515
beforeAll(async () => {
@@ -85,4 +85,4 @@ describe("createPaymentHeader", () => {
8585
);
8686
});
8787
});
88-
});
88+
});

typescript/packages/x402/src/facilitator/facilitator.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ExactEvmPayload,
1616
} from "../types/verify";
1717
import { Chain, Transport, Account } from "viem";
18-
import { KeyPairSigner } from "@solana/kit";
18+
import { TransactionSigner } from "@solana/kit";
1919

2020
/**
2121
* Verifies a payment payload against the required payment details regardless of the scheme
@@ -50,7 +50,12 @@ export async function verify<
5050

5151
// svm
5252
if (SupportedSVMNetworks.includes(paymentRequirements.network)) {
53-
return await verifyExactSvm(client as KeyPairSigner, payload, paymentRequirements, config);
53+
return await verifyExactSvm(
54+
client as TransactionSigner,
55+
payload,
56+
paymentRequirements,
57+
config,
58+
);
5459
}
5560
}
5661

@@ -93,7 +98,12 @@ export async function settle<transport extends Transport, chain extends Chain>(
9398

9499
// svm
95100
if (SupportedSVMNetworks.includes(paymentRequirements.network)) {
96-
return await settleExactSvm(client as KeyPairSigner, payload, paymentRequirements, config);
101+
return await settleExactSvm(
102+
client as TransactionSigner,
103+
payload,
104+
paymentRequirements,
105+
config,
106+
);
97107
}
98108
}
99109

typescript/packages/x402/src/schemes/exact/evm/client.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,10 @@ describe("signPaymentHeader", () => {
166166
expect(result.x402Version).toBe(mockUnsignedHeader.x402Version);
167167
expect(result.scheme).toBe(mockUnsignedHeader.scheme);
168168
expect(result.network).toBe(mockUnsignedHeader.network);
169-
expect(result.payload.authorization).toEqual(mockUnsignedHeader.payload.authorization);
169+
expect("authorization" in result.payload).toBe(true);
170+
if ("authorization" in result.payload) {
171+
expect(result.payload.authorization).toEqual(mockUnsignedHeader.payload.authorization);
172+
}
170173
});
171174

172175
it("should throw an error if signing fails", async () => {

typescript/packages/x402/src/schemes/exact/evm/utils/paymentUtils.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ const validEvmPayload: ExactEvmPayload = {
1717
};
1818

1919
// valid evm payment payload
20+
const defaultEvmNetwork = SupportedEVMNetworks[0] as (typeof SupportedEVMNetworks)[number];
21+
const defaultSvmNetwork = SupportedSVMNetworks[0] as (typeof SupportedSVMNetworks)[number];
22+
2023
const validEvmPayment: PaymentPayload = {
2124
x402Version: 1,
2225
scheme: "exact",
23-
network: SupportedEVMNetworks[0],
26+
network: defaultEvmNetwork,
2427
payload: validEvmPayload,
2528
};
2629

@@ -33,7 +36,7 @@ const validSvmPayload: ExactSvmPayload = {
3336
const validSvmPayment: PaymentPayload = {
3437
x402Version: 1,
3538
scheme: "exact",
36-
network: SupportedSVMNetworks[0],
39+
network: defaultSvmNetwork,
3740
payload: validSvmPayload,
3841
};
3942

@@ -51,12 +54,18 @@ describe("paymentUtils", () => {
5154
});
5255

5356
it("throws on invalid network in encodePayment", () => {
54-
const invalidPayment = { ...validEvmPayment, network: "invalid-network" };
57+
const invalidPayment = {
58+
...validEvmPayment,
59+
network: "invalid-network",
60+
} as unknown as PaymentPayload;
5561
expect(() => encodePayment(invalidPayment)).toThrow("Invalid network");
5662
});
5763

5864
it("throws on invalid network in decodePayment", () => {
59-
const invalid = { ...validEvmPayment, network: "invalid-network" };
65+
const invalid = {
66+
...validEvmPayment,
67+
network: "invalid-network",
68+
} as unknown as PaymentPayload;
6069
const encoded = Buffer.from(JSON.stringify(invalid)).toString("base64");
6170
expect(() => decodePayment(encoded)).toThrow("Invalid network");
6271
});

typescript/packages/x402/src/schemes/exact/svm/client.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { afterEach, beforeAll, describe, expect, it, vi, beforeEach } from "vitest";
3-
import { type Address, type KeyPairSigner, generateKeyPairSigner, lamports } from "@solana/kit";
3+
import { type Address, type TransactionSigner, generateKeyPairSigner, lamports } from "@solana/kit";
44
import * as solanaKit from "@solana/kit";
55
import * as token2022 from "@solana-program/token-2022";
66
import * as token from "@solana-program/token";
@@ -54,7 +54,7 @@ vi.mock("@solana-program/compute-budget", async importOriginal => {
5454
});
5555

5656
describe("SVM Client", () => {
57-
let clientSigner: KeyPairSigner;
57+
let clientSigner: TransactionSigner;
5858
let paymentRequirements: PaymentRequirements;
5959
const mockRpcClient = {
6060
getLatestBlockhash: vi.fn().mockReturnValue({

typescript/packages/x402/src/schemes/exact/svm/client.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
partiallySignTransactionMessageWithSigners,
1010
prependTransactionMessageInstruction,
1111
getBase64EncodedWireTransaction,
12-
type KeyPairSigner,
1312
fetchEncodedAccount,
1413
TransactionSigner,
1514
Instruction,
@@ -41,7 +40,7 @@ import { getRpcClient } from "../../../shared/svm/rpc";
4140
* @returns A promise that resolves to a base64 encoded payment header string
4241
*/
4342
export async function createPaymentHeader(
44-
client: KeyPairSigner,
43+
client: TransactionSigner,
4544
x402Version: number,
4645
paymentRequirements: PaymentRequirements,
4746
config?: X402Config,
@@ -65,7 +64,7 @@ export async function createPaymentHeader(
6564
* @returns A promise that resolves to a payment payload containing a base64 encoded solana token transfer tx
6665
*/
6766
export async function createAndSignPayment(
68-
client: KeyPairSigner,
67+
client: TransactionSigner,
6968
x402Version: number,
7069
paymentRequirements: PaymentRequirements,
7170
config?: X402Config,
@@ -98,7 +97,7 @@ export async function createAndSignPayment(
9897
* @returns A promise that resolves to the transaction message with the transfer instruction
9998
*/
10099
async function createTransferTransactionMessage(
101-
client: KeyPairSigner,
100+
client: TransactionSigner,
102101
paymentRequirements: PaymentRequirements,
103102
config?: X402Config,
104103
) {
@@ -150,7 +149,7 @@ async function createTransferTransactionMessage(
150149
* @returns A promise that resolves to the create ATA (if needed) and transfer instruction
151150
*/
152151
async function createAtaAndTransferInstructions(
153-
client: KeyPairSigner,
152+
client: TransactionSigner,
154153
paymentRequirements: PaymentRequirements,
155154
config?: X402Config,
156155
): Promise<Instruction[]> {
@@ -260,7 +259,7 @@ async function createAtaInstructionOrUndefined(
260259
* @returns A promise that resolves to the transfer instruction
261260
*/
262261
async function createTransferInstruction(
263-
client: KeyPairSigner,
262+
client: TransactionSigner,
264263
paymentRequirements: PaymentRequirements,
265264
decimals: number,
266265
tokenProgramAddress: Address,

typescript/packages/x402/src/schemes/exact/svm/facilitator/settle.test.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3-
import { type KeyPairSigner, generateKeyPairSigner } from "@solana/kit";
3+
import { type TransactionSigner, generateKeyPairSigner } from "@solana/kit";
44
import * as solanaKit from "@solana/kit";
55
import * as transactionConfirmation from "@solana/transaction-confirmation";
66
import { PaymentPayload, PaymentRequirements, ExactSvmPayload } from "../../../../types/verify";
7-
import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../../shared/svm";
7+
import {
8+
decodeTransactionFromPayload,
9+
getTokenPayerFromTransaction,
10+
signTransactionWithSigner,
11+
} from "../../../../shared/svm";
812
import { getRpcClient, getRpcSubscriptions } from "../../../../shared/svm/rpc";
913
import { verify } from "./verify";
1014
import * as settleModule from "./settle";
@@ -43,7 +47,7 @@ vi.mock("@solana/transaction-confirmation", async importOriginal => {
4347
});
4448

4549
describe("SVM Settle", () => {
46-
let signer: KeyPairSigner;
50+
let signer: TransactionSigner;
4751
let payerAddress: string;
4852
let paymentPayload: PaymentPayload;
4953
let paymentRequirements: PaymentRequirements;
@@ -132,6 +136,7 @@ describe("SVM Settle", () => {
132136
version: 0,
133137
} as any);
134138
vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress);
139+
vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction);
135140
vi.mocked(transactionConfirmation.waitForRecentTransactionConfirmation).mockResolvedValue(
136141
undefined,
137142
);
@@ -142,11 +147,8 @@ describe("SVM Settle", () => {
142147
// Assert
143148
expect(verify).toHaveBeenCalledWith(signer, paymentPayload, paymentRequirements, undefined);
144149
expect(decodeTransactionFromPayload).toHaveBeenCalledWith(paymentPayload.payload);
150+
expect(signTransactionWithSigner).toHaveBeenCalledWith(signer, mockSignedTransaction);
145151
expect(transactionConfirmation.waitForRecentTransactionConfirmation).toHaveBeenCalledOnce();
146-
expect(solanaKit.signTransaction).toHaveBeenCalledWith(
147-
[signer.keyPair],
148-
mockSignedTransaction,
149-
);
150152
expect(mockRpcClient.sendTransaction).toHaveBeenCalled();
151153
expect(result).toEqual({
152154
success: true,
@@ -189,6 +191,7 @@ describe("SVM Settle", () => {
189191
vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress);
190192
vi.mocked(getRpcClient).mockReturnValue(mockRpcClient);
191193
vi.mocked(getRpcSubscriptions).mockReturnValue(mockRpcSubscriptions);
194+
vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction);
192195
// Mock the sendAndConfirmSignedTransaction to throw an error
193196
vi.mocked(mockRpcClient.sendTransaction).mockReturnValue({
194197
send: vi.fn().mockRejectedValue(new Error("Unexpected error")),

typescript/packages/x402/src/schemes/exact/svm/facilitator/settle.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@ import {
1414
getCompiledTransactionMessageDecoder,
1515
getSignatureFromTransaction,
1616
isSolanaError,
17-
KeyPairSigner,
17+
type Transaction,
18+
type TransactionSigner,
1819
SendTransactionApi,
19-
signTransaction,
2020
SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED,
2121
SolanaRpcApiDevnet,
2222
SolanaRpcApiMainnet,
2323
RpcDevnet,
2424
RpcMainnet,
2525
} from "@solana/kit";
26-
import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../../shared/svm";
26+
import {
27+
decodeTransactionFromPayload,
28+
getTokenPayerFromTransaction,
29+
signTransactionWithSigner,
30+
} from "../../../../shared/svm";
2731
import { getRpcClient, getRpcSubscriptions } from "../../../../shared/svm/rpc";
2832
import {
2933
createBlockHeightExceedencePromiseFactory,
@@ -43,7 +47,7 @@ import { verify } from "./verify";
4347
* @returns A SettleResponse indicating if the payment is settled and any error reason
4448
*/
4549
export async function settle(
46-
signer: KeyPairSigner,
50+
signer: TransactionSigner,
4751
payload: PaymentPayload,
4852
paymentRequirements: PaymentRequirements,
4953
config?: X402Config,
@@ -60,8 +64,9 @@ export async function settle(
6064

6165
const svmPayload = payload.payload as ExactSvmPayload;
6266
const decodedTransaction = decodeTransactionFromPayload(svmPayload);
63-
const signedTransaction = await signTransaction([signer.keyPair], decodedTransaction);
64-
const payer = getTokenPayerFromTransaction(decodedTransaction);
67+
const signedTransaction = await signTransactionWithSigner(signer, decodedTransaction);
68+
assertTransactionFullySigned(signedTransaction);
69+
const payer = getTokenPayerFromTransaction(signedTransaction);
6570

6671
const rpc = getRpcClient(paymentRequirements.network, config?.svmConfig?.rpcUrl);
6772
const rpcSubscriptions = getRpcSubscriptions(
@@ -105,7 +110,7 @@ export async function settle(
105110
* @returns The signature of the sent transaction
106111
*/
107112
export async function sendSignedTransaction(
108-
signedTransaction: Awaited<ReturnType<typeof signTransaction>>,
113+
signedTransaction: Transaction,
109114
rpc: RpcDevnet<SolanaRpcApiDevnet> | RpcMainnet<SolanaRpcApiMainnet>,
110115
sendTxConfig: Parameters<SendTransactionApi["sendTransaction"]>[1] = {
111116
skipPreflight: true,
@@ -127,7 +132,7 @@ export async function sendSignedTransaction(
127132
* @returns The success and signature of the confirmed transaction
128133
*/
129134
export async function confirmSignedTransaction(
130-
signedTransaction: Awaited<ReturnType<typeof signTransaction>>,
135+
signedTransaction: Transaction,
131136
rpc: RpcDevnet<SolanaRpcApiDevnet> | RpcMainnet<SolanaRpcApiMainnet>,
132137
rpcSubscriptions: ReturnType<typeof getRpcSubscriptions>,
133138
): Promise<{ success: boolean; errorReason?: (typeof ErrorReasons)[number]; signature: string }> {
@@ -180,7 +185,9 @@ export async function confirmSignedTransaction(
180185
// wait for the transaction to be confirmed
181186
await waitForRecentTransactionConfirmation({
182187
...config,
183-
transaction: signedTransactionWithBlockhashLifetime,
188+
transaction: signedTransactionWithBlockhashLifetime as Parameters<
189+
typeof waitForRecentTransactionConfirmation
190+
>[0]["transaction"],
184191
});
185192

186193
// return the success and signature
@@ -226,10 +233,25 @@ export async function confirmSignedTransaction(
226233
* @returns The success and signature of the confirmed transaction
227234
*/
228235
export async function sendAndConfirmSignedTransaction(
229-
signedTransaction: Awaited<ReturnType<typeof signTransaction>>,
236+
signedTransaction: Transaction,
230237
rpc: RpcDevnet<SolanaRpcApiDevnet> | RpcMainnet<SolanaRpcApiMainnet>,
231238
rpcSubscriptions: ReturnType<typeof getRpcSubscriptions>,
232239
): Promise<{ success: boolean; errorReason?: (typeof ErrorReasons)[number]; signature: string }> {
233240
await sendSignedTransaction(signedTransaction, rpc);
234241
return await confirmSignedTransaction(signedTransaction, rpc, rpcSubscriptions);
235242
}
243+
244+
/**
245+
* Ensures the provided transaction contains a signature for every required address.
246+
*
247+
* @param transaction - Transaction to verify for complete signatures
248+
*/
249+
function assertTransactionFullySigned(transaction: Transaction): void {
250+
const missingAddresses = Object.entries(transaction.signatures)
251+
.filter(([, signature]) => signature == null)
252+
.map(([address]) => address);
253+
254+
if (missingAddresses.length > 0) {
255+
throw new Error(`transaction_signer_missing_signatures:${missingAddresses.join(",")}`);
256+
}
257+
}

typescript/packages/x402/src/schemes/exact/svm/facilitator/verify.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
verifyComputePriceInstruction,
1111
} from "./verify";
1212
import {
13-
KeyPairSigner,
13+
type TransactionSigner,
1414
assertIsInstructionWithData,
1515
assertIsInstructionWithAccounts,
1616
decompileTransactionMessage,
@@ -398,7 +398,7 @@ describe("verify", () => {
398398
});
399399

400400
describe("verify high level flow", () => {
401-
let mockSigner: KeyPairSigner;
401+
let mockSigner: TransactionSigner;
402402
let mockPayerAddress: string;
403403
let mockPayload: PaymentPayload;
404404
let mockRequirements: PaymentRequirements;
@@ -814,8 +814,8 @@ describe("verify", () => {
814814

815815
const mockSigner = {
816816
address: "TestSigner1111111111111111111111111111" as any,
817-
keyPair: {} as any,
818-
} as KeyPairSigner;
817+
signTransactions: vi.fn().mockResolvedValue([{}] as any),
818+
} as TransactionSigner;
819819

820820
beforeEach(() => {
821821
vi.clearAllMocks();

typescript/packages/x402/src/schemes/exact/svm/facilitator/verify.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
decompileTransactionMessage,
1616
fetchEncodedAccounts,
1717
getCompiledTransactionMessageDecoder,
18-
KeyPairSigner,
18+
type TransactionSigner,
1919
SolanaRpcApiDevnet,
2020
SolanaRpcApiMainnet,
2121
RpcDevnet,
@@ -62,7 +62,7 @@ import { SCHEME } from "../../";
6262
* @returns A VerifyResponse indicating if the payment is valid and any invalidation reason
6363
*/
6464
export async function verify(
65-
signer: KeyPairSigner,
65+
signer: TransactionSigner,
6666
payload: PaymentPayload,
6767
paymentRequirements: PaymentRequirements,
6868
config?: X402Config,

0 commit comments

Comments
 (0)