Skip to content

Commit 3993ce5

Browse files
fix(wallets): normalize Solana transaction encodings
Co-Authored-By: Alfonso <alfonso@paella.dev>
1 parent 558be6e commit 3993ce5

11 files changed

Lines changed: 84 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@crossmint/wallets-sdk": minor
3+
---
4+
5+
Solana `serializedTransaction` values now accept base58 or base64 encoding with automatic detection. Solana web3.js transaction types are re-exported, and the `@solana/web3.js` dependency range is relaxed to reduce duplicate-copy type clashes.

packages/wallets/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ const client = evmWallet.getViemClient();
203203
// Solana
204204
const solWallet = SolanaWallet.from(wallet);
205205
const solTx = await solWallet.sendTransaction({
206-
serializedTransaction: "<base64-encoded-transaction>",
206+
serializedTransaction: "<base58-or-base64-encoded-transaction>",
207207
});
208208

209209
// Stellar

packages/wallets/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@crossmint/common-sdk-auth": "workspace:*",
3232
"@crossmint/common-sdk-base": "workspace:*",
3333
"@hey-api/client-fetch": "0.8.1",
34-
"@solana/web3.js": "1.98.1",
34+
"@solana/web3.js": "^1.98.1",
3535
"abitype": "1.0.8",
3636
"base32.js": "0.1.0",
3737
"bs58": "5.0.0",

packages/wallets/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export type {
4545
RemoveSignerOptions,
4646
} from "./wallets/types";
4747
export type { Chain, EVMChain, SolanaChain, StellarChain } from "./chains/chains";
48+
export type {
49+
VersionedTransaction,
50+
Transaction as SolanaTransaction,
51+
Keypair as SolanaKeypair,
52+
PublicKey as SolanaPublicKey,
53+
} from "@solana/web3.js";
4854

4955
// Signer configuration types
5056
export {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { MessageV0, PublicKey, VersionedTransaction } from "@solana/web3.js";
2+
import bs58 from "bs58";
3+
import { describe, expect, it } from "vitest";
4+
import { normalizeSolanaSerializedTransaction } from "./solana-transaction";
5+
6+
describe("normalizeSolanaSerializedTransaction", () => {
7+
const transaction = new VersionedTransaction(
8+
MessageV0.compile({
9+
payerKey: PublicKey.default,
10+
instructions: [],
11+
recentBlockhash: PublicKey.default.toBase58(),
12+
})
13+
);
14+
const serializedBytes = transaction.serialize();
15+
const base58SerializedTransaction = bs58.encode(serializedBytes);
16+
17+
it("returns a base58-encoded transaction unchanged", () => {
18+
expect(normalizeSolanaSerializedTransaction(base58SerializedTransaction)).toBe(base58SerializedTransaction);
19+
});
20+
21+
it("converts a base64-encoded transaction to base58", () => {
22+
const base64SerializedTransaction = Buffer.from(serializedBytes).toString("base64");
23+
24+
expect(normalizeSolanaSerializedTransaction(base64SerializedTransaction)).toBe(
25+
bs58.encode(transaction.serialize())
26+
);
27+
});
28+
29+
it("throws for an invalid serialized transaction", () => {
30+
expect(() => normalizeSolanaSerializedTransaction("garbage")).toThrow(
31+
"Value must be a base58- or base64-encoded Solana transaction"
32+
);
33+
});
34+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { VersionedTransaction } from "@solana/web3.js";
2+
import bs58 from "bs58";
3+
4+
export function normalizeSolanaSerializedTransaction(serialized: string): string {
5+
try {
6+
VersionedTransaction.deserialize(bs58.decode(serialized));
7+
return serialized;
8+
} catch {
9+
try {
10+
const bytes = Buffer.from(serialized, "base64");
11+
VersionedTransaction.deserialize(bytes);
12+
return bs58.encode(bytes);
13+
} catch {
14+
throw new Error("Value must be a base58- or base64-encoded Solana transaction");
15+
}
16+
}
17+
}

packages/wallets/src/wallets/__tests__/test-helpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { vi, type MockedFunction } from "vitest";
22
import { APIKeyEnvironmentPrefix } from "@crossmint/common-sdk-base";
3+
import { MessageV0, PublicKey, VersionedTransaction } from "@solana/web3.js";
4+
import bs58 from "bs58";
35
import { Wallet } from "../wallet";
46
import type { ApiClient } from "../../api";
57
import type { Chain } from "../../chains/chains";
@@ -110,5 +112,10 @@ export const createMockApiClient = (overrides: Partial<MockedApiClient> = {}): M
110112
});
111113

112114
export const createMockSolanaSerializedTransaction = (): string => {
113-
return "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEDBQrKxEIIPWsDwcGCzLQ7FGIHQ38p0dZq6bG2v2wUAUqMx3jV1jZ0";
115+
const message = MessageV0.compile({
116+
payerKey: PublicKey.default,
117+
instructions: [],
118+
recentBlockhash: PublicKey.default.toBase58(),
119+
});
120+
return bs58.encode(new VersionedTransaction(message).serialize());
114121
};

packages/wallets/src/wallets/solana.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import bs58 from "bs58";
23
import { SolanaWallet } from "./solana";
34
import type { CreateTransactionSuccessResponse } from "../api";
45
import { TransactionNotCreatedError } from "../utils/errors";
@@ -31,7 +32,7 @@ describe("SolanaWallet - sendTransaction()", () => {
3132
});
3233

3334
describe("success cases", () => {
34-
it("sends transaction with serialized transaction string", async () => {
35+
it("sends transaction with a base58 serialized transaction string", async () => {
3536
const serializedTx = createMockSolanaSerializedTransaction();
3637

3738
const mockTransactionResponse = {
@@ -75,9 +76,9 @@ describe("SolanaWallet - sendTransaction()", () => {
7576
);
7677
});
7778

78-
it("sends transaction with serialized transaction string", async () => {
79-
const serializedTx =
80-
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEDBQrKxEIIPWsDwcGCzLQ7FGIHQ38p0dZq6bG2v2wUAUqMx3jV1jZ0";
79+
it("converts a base64 serialized transaction to base58", async () => {
80+
const base58SerializedTx = createMockSolanaSerializedTransaction();
81+
const serializedTx = Buffer.from(bs58.decode(base58SerializedTx)).toString("base64");
8182

8283
const mockTransactionResponse = {
8384
id: "txn-sol-456",
@@ -90,7 +91,7 @@ describe("SolanaWallet - sendTransaction()", () => {
9091
"https://explorer.solana.com/tx/5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW",
9192
},
9293
params: {
93-
transaction: serializedTx,
94+
transaction: base58SerializedTx,
9495
signer: "api-key:test",
9596
},
9697
createdAt: Date.now(),
@@ -113,7 +114,7 @@ describe("SolanaWallet - sendTransaction()", () => {
113114
"me:solana:smart",
114115
expect.objectContaining({
115116
params: expect.objectContaining({
116-
transaction: serializedTx,
117+
transaction: base58SerializedTx,
117118
signer: "api-key",
118119
}),
119120
})

packages/wallets/src/wallets/solana.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TransactionNotCreatedError } from "../utils/errors";
1313
import { SolanaExternalWalletSigner } from "@/signers/solana-external-wallet";
1414
import type { CreateTransactionSuccessResponse } from "@/api";
1515
import { walletsLogger } from "../logger";
16+
import { normalizeSolanaSerializedTransaction } from "../utils/solana-transaction";
1617

1718
export class SolanaWallet extends Wallet<SolanaChain> {
1819
constructor(wallet: Wallet<SolanaChain>) {
@@ -43,7 +44,7 @@ export class SolanaWallet extends Wallet<SolanaChain> {
4344

4445
/**
4546
* Send a raw Solana transaction.
46-
* @param params - The transaction parameters (serialized transaction or Transaction object)
47+
* @param params - The transaction parameters. Serialized transactions may be base58- or base64-encoded.
4748
* @returns The transaction result
4849
*/
4950
@WithLoggerContext({
@@ -110,7 +111,7 @@ export class SolanaWallet extends Wallet<SolanaChain> {
110111
let serializedTransaction: string;
111112

112113
if ("serializedTransaction" in params) {
113-
serializedTransaction = params.serializedTransaction;
114+
serializedTransaction = normalizeSolanaSerializedTransaction(params.serializedTransaction);
114115
} else {
115116
serializedTransaction = bs58.encode(params.transaction.serialize());
116117
}

packages/wallets/src/wallets/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export type SolanaTransactionInput = (
112112
transaction: VersionedTransaction;
113113
}
114114
| {
115+
/** A base58- or base64-encoded serialized Solana transaction. */
115116
serializedTransaction: string;
116117
}
117118
) & {

0 commit comments

Comments
 (0)