From 1d9c5b8ea70c658d393e1fa6d3c064fd5bf95803 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Thu, 22 Aug 2024 19:27:07 -0400 Subject: [PATCH 1/5] update solana to allow multiple client types --- packages/solana/package.json | 4 +- packages/solana/src/index.ts | 131 +++++++++++++++++++++++------------ pnpm-lock.yaml | 6 ++ 3 files changed, 96 insertions(+), 45 deletions(-) diff --git a/packages/solana/package.json b/packages/solana/package.json index 2b4fd376a..07d57c609 100644 --- a/packages/solana/package.json +++ b/packages/solana/package.json @@ -49,7 +49,9 @@ }, "dependencies": { "@solana/web3.js": "^1.88.1", - "@turnkey/http": "workspace:*" + "@turnkey/http": "workspace:*", + "@turnkey/sdk-browser": "workspace:*", + "@turnkey/sdk-server": "workspace:*" }, "devDependencies": { "@turnkey/api-key-stamper": "workspace:*", diff --git a/packages/solana/src/index.ts b/packages/solana/src/index.ts index 87d028303..9f9346f24 100644 --- a/packages/solana/src/index.ts +++ b/packages/solana/src/index.ts @@ -4,12 +4,22 @@ import { type VersionedTransaction, } from "@solana/web3.js"; import { TurnkeyActivityError, TurnkeyClient } from "@turnkey/http"; +import type { TurnkeyBrowserClient } from "@turnkey/sdk-browser"; +import type { TurnkeyServerClient } from "@turnkey/sdk-server"; + +type TSignature = { + r: string; + s: string; + v: string; +}; + +type TClient = TurnkeyClient | TurnkeyBrowserClient | TurnkeyServerClient; export class TurnkeySigner { public readonly organizationId: string; - public readonly client: TurnkeyClient; + public readonly client: TClient; - constructor(input: { organizationId: string; client: TurnkeyClient }) { + constructor(input: { organizationId: string; client: TClient }) { this.organizationId = input.organizationId; this.client = input.client; } @@ -33,10 +43,9 @@ export class TurnkeySigner { fromAddress ); - const signatures = - signRawPayloadsResult.signRawPayloadsResult?.signatures?.map( - (sig) => `${sig?.r}${sig?.s}` - ); + const signatures = signRawPayloadsResult?.signatures?.map( + (sig: TSignature) => `${sig?.r}${sig?.s}` + ); for (let i in txs) { txs[i]?.addSignature(fromKey, Buffer.from(signatures![i]!, "hex")); @@ -64,7 +73,7 @@ export class TurnkeySigner { fromAddress ); - const signature = `${signRawPayloadResult.signRawPayloadResult?.r}${signRawPayloadResult.signRawPayloadResult?.s}`; + const signature = `${signRawPayloadResult?.r}${signRawPayloadResult?.s}`; tx.addSignature(fromKey, Buffer.from(signature, "hex")); } @@ -84,67 +93,93 @@ export class TurnkeySigner { fromAddress ); return Buffer.from( - `${signRawPayloadResult.signRawPayloadResult?.r}${signRawPayloadResult.signRawPayloadResult?.s}`, + `${signRawPayloadResult?.r}${signRawPayloadResult?.s}`, "hex" ); } private async signRawPayload(payload: string, signWith: string) { - const response = await this.client.signRawPayload({ - type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", - organizationId: this.organizationId, - timestampMs: String(Date.now()), - parameters: { + if (this.client instanceof TurnkeyClient) { + const response = await this.client.signRawPayload({ + type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2", + organizationId: this.organizationId, + timestampMs: String(Date.now()), + parameters: { + signWith, + payload, + encoding: "PAYLOAD_ENCODING_HEXADECIMAL", + // Note: unlike ECDSA, EdDSA's API does not support signing raw digests (see RFC 8032). + // Turnkey's signer requires an explicit value to be passed here to minimize ambiguity. + hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", + }, + }); + + const { id, status, type, result } = response.activity; + + if (status !== "ACTIVITY_STATUS_COMPLETED") { + throw new TurnkeyActivityError({ + message: `Expected COMPLETED status, got ${status}`, + activityId: id, + activityStatus: status, + activityType: type, + }); + } + + return assertNonNull(result?.signRawPayloadResult); + } else { + const result = await this.client.signRawPayload({ signWith, payload, encoding: "PAYLOAD_ENCODING_HEXADECIMAL", // Note: unlike ECDSA, EdDSA's API does not support signing raw digests (see RFC 8032). // Turnkey's signer requires an explicit value to be passed here to minimize ambiguity. hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", - }, - }); - - const { id, status, type, result } = response.activity; - - if (status !== "ACTIVITY_STATUS_COMPLETED") { - throw new TurnkeyActivityError({ - message: `Expected COMPLETED status, got ${status}`, - activityId: id, - activityStatus: status, - activityType: type, }); - } - return result; + return assertNonNull(result); + } } private async signRawPayloads(payloads: string[], signWith: string) { - const response = await this.client.signRawPayloads({ - type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOADS", - organizationId: this.organizationId, - timestampMs: String(Date.now()), - parameters: { + if (this.client instanceof TurnkeyClient) { + const response = await this.client.signRawPayloads({ + type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOADS", + organizationId: this.organizationId, + timestampMs: String(Date.now()), + parameters: { + signWith, + payloads, + encoding: "PAYLOAD_ENCODING_HEXADECIMAL", + // Note: unlike ECDSA, EdDSA's API does not support signing raw digests (see RFC 8032). + // Turnkey's signer requires an explicit value to be passed here to minimize ambiguity. + hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", + }, + }); + + const { id, status, type, result } = response.activity; + + if (status !== "ACTIVITY_STATUS_COMPLETED") { + throw new TurnkeyActivityError({ + message: `Expected COMPLETED status, got ${status}`, + activityId: id, + activityStatus: status, + activityType: type, + }); + } + + return assertNonNull(result?.signRawPayloadsResult); + } else { + const result = await this.client.signRawPayloads({ signWith, payloads, encoding: "PAYLOAD_ENCODING_HEXADECIMAL", // Note: unlike ECDSA, EdDSA's API does not support signing raw digests (see RFC 8032). // Turnkey's signer requires an explicit value to be passed here to minimize ambiguity. hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", - }, - }); - - const { id, status, type, result } = response.activity; - - if (status !== "ACTIVITY_STATUS_COMPLETED") { - throw new TurnkeyActivityError({ - message: `Expected COMPLETED status, got ${status}`, - activityId: id, - activityStatus: status, - activityType: type, }); - } - return result; + return assertNonNull(result); + } } private getMessageToSign(tx: Transaction | VersionedTransaction): Buffer { @@ -165,3 +200,11 @@ export class TurnkeySigner { return messageToSign; } } + +function assertNonNull(input: T | null | undefined): T { + if (input == null) { + throw new Error(`Got unexpected ${JSON.stringify(input)}`); + } + + return input; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e0b453e8..f92445f04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1131,6 +1131,12 @@ importers: '@turnkey/http': specifier: workspace:* version: link:../http + '@turnkey/sdk-browser': + specifier: workspace:* + version: link:../sdk-browser + '@turnkey/sdk-server': + specifier: workspace:* + version: link:../sdk-server devDependencies: '@turnkey/api-key-stamper': specifier: workspace:* From 1605826d53e3692b55e8374be2ed007d3f9f26b7 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 26 Aug 2024 17:02:25 -0400 Subject: [PATCH 2/5] add tests --- packages/solana/src/__tests__/index-test.ts | 577 +++++++++----------- 1 file changed, 262 insertions(+), 315 deletions(-) diff --git a/packages/solana/src/__tests__/index-test.ts b/packages/solana/src/__tests__/index-test.ts index 23615a7a0..d90da5a3d 100644 --- a/packages/solana/src/__tests__/index-test.ts +++ b/packages/solana/src/__tests__/index-test.ts @@ -2,6 +2,7 @@ import { test, expect, describe } from "@jest/globals"; import { TurnkeySigner } from "../"; import { TurnkeyClient } from "@turnkey/http"; import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { Turnkey } from "@turnkey/sdk-server"; import { PublicKey, SIGNATURE_LENGTH_IN_BYTES, @@ -18,331 +19,277 @@ const TKHQ_WARCHEST = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; const DEFAULT_SIGNATURE = new Uint8Array(SIGNATURE_LENGTH_IN_BYTES); describe("TurnkeySigner", () => { + if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { + // These tests requires an env var to be set + throw new Error( + "These tests require SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" + ); + } + const organizationId = "4456e4c2-e8b5-4b93-a0cf-1dfae265c12c"; const apiPublicKey = "025d374c674fc389c761462f3c59c0acabdcb3a17c599d9e62e5fe78fe984cfbeb"; const turnkeySolAddress = "D8P541wwnertZTgDT14kYJPoFT2eHUFqjTgPxMK5qatM"; - test("can sign a Solana transfer against production", async () => { - if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { - // This test requires an env var to be set - throw new Error( - "This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" - ); - } - - const client = new TurnkeyClient( - { baseUrl: "https://api.turnkey.com" }, - new ApiKeyStamper({ - apiPublicKey, - apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, - }) - ); - - const signer = new TurnkeySigner({ - organizationId, - client, - }); - - const transferTransaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: new PublicKey(turnkeySolAddress), - // Destination doesn't matter, we set it to the Turnkey war chest! - toPubkey: new PublicKey(TKHQ_WARCHEST), - lamports: 10000, - }) - ); - - // Doesn't really matter since we're not going to broadcast this transaction! - // But if we don't set this the call to "serializeMessage fails." - transferTransaction.recentBlockhash = DEFAULT_BLOCK_HASH; - transferTransaction.feePayer = new PublicKey(turnkeySolAddress); - - expect(transferTransaction.signatures.length).toBe(0); - await signer.addSignature(transferTransaction, turnkeySolAddress); - expect(transferTransaction.signatures.length).toBe(1); - - const isValidSignature = nacl.sign.detached.verify( - transferTransaction.serializeMessage(), - transferTransaction.signature as Uint8Array, - bs58.decode(turnkeySolAddress) - ); - expect(isValidSignature).toBeTruthy(); + const turnkeyBaseClient = new TurnkeyClient( + { baseUrl: "https://api.turnkey.com" }, + new ApiKeyStamper({ + apiPublicKey, + apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, + }) + ); + + const turnkeyServerClient = new Turnkey({ + apiBaseUrl: "https://api.turnkey.com", + apiPublicKey, + apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, + defaultOrganizationId: organizationId, }); - test("can sign a versioned Solana transfer against production", async () => { - if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { - // This test requires an env var to be set - throw new Error( - "This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" - ); - } - - const client = new TurnkeyClient( - { baseUrl: "https://api.turnkey.com" }, - new ApiKeyStamper({ - apiPublicKey, - apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, - }) - ); - - const signer = new TurnkeySigner({ - organizationId, - client, - }); - - const fromKey = new PublicKey(turnkeySolAddress); - - const instructions = [ - SystemProgram.transfer({ - fromPubkey: fromKey, - // Destination doesn't matter, we set it to the Turnkey war chest! - toPubkey: new PublicKey(TKHQ_WARCHEST), - lamports: 10, + [ + { + configName: "Base HTTP Turnkey client", + signer: new TurnkeySigner({ + organizationId, + client: turnkeyBaseClient, }), - ]; - - // create v0 compatible message - const messageV0 = new TransactionMessage({ - payerKey: fromKey, - // Doesn't really matter since we're not going to broadcast this transaction! - recentBlockhash: DEFAULT_BLOCK_HASH, - instructions, - }).compileToV0Message(); - - const transaction = new VersionedTransaction(messageV0); - - // version transactions are initialized with a default signature - expect(transaction.signatures.length).toBe(1); - expect(transaction.signatures[0]).toEqual(DEFAULT_SIGNATURE); - - await signer.addSignature(transaction, turnkeySolAddress); - - // after signing the version transaction, the default signature is replaced with the new one - expect(transaction.signatures.length).toBe(1); - expect(transaction.signatures[0]).not.toEqual(DEFAULT_SIGNATURE); - - const isValidSignature = nacl.sign.detached.verify( - transaction.message.serialize(), - transaction.signatures[0] as Uint8Array, - bs58.decode(turnkeySolAddress) - ); - expect(isValidSignature).toBeTruthy(); - }); - - test("can sign multiple Solana transfers against production", async () => { - if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { - // This test requires an env var to be set - throw new Error( - "This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" - ); - } - - const client = new TurnkeyClient( - { baseUrl: "https://api.turnkey.com" }, - new ApiKeyStamper({ - apiPublicKey, - apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, - }) - ); - - const signer = new TurnkeySigner({ - organizationId, - client, - }); - - const numTxs = 3; - const transactions = new Array(); - const amounts = new Array(); - - for (let i = 0; i < numTxs; i++) { - const amount = Math.floor(Math.random() * 100); // random amount - amounts.push(amount); - - const transferTransaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: new PublicKey(turnkeySolAddress), - // Destination doesn't matter, we set it to the Turnkey war chest! - toPubkey: new PublicKey(TKHQ_WARCHEST), - lamports: amount, - }) - ); - - expect(transferTransaction.signatures.length).toBe(0); - - // Doesn't really matter since we're not going to broadcast this transaction! - // But if we don't set this the call to "serializeMessage fails." - transferTransaction.recentBlockhash = DEFAULT_BLOCK_HASH; - transferTransaction.feePayer = new PublicKey(turnkeySolAddress); - - transactions.push(transferTransaction); - } - - const signedTransactions = await signer.signAllTransactions( - transactions, - turnkeySolAddress - ); - expect(signedTransactions.length).toBe(numTxs); - - for (let i = 0; i < signedTransactions.length; i++) { - const tx = signedTransactions[i] as Transaction; - - // Verify the signature itself - const isValidSignature = nacl.sign.detached.verify( - tx.serializeMessage(), - tx.signature as Uint8Array, - bs58.decode(turnkeySolAddress) - ); - expect(isValidSignature).toBeTruthy(); - - // Ensure it's a simple, native transfer - expect(tx.instructions.length).toEqual(1); - - const programId = tx.instructions[0]!.programId!; - const data = tx.instructions[0]!.data!; - - expect(programId).toEqual(SystemProgram.programId); - expect(data[0]).toEqual(2); - - // Convert raw data to lamports, then to whole SOL units - const amountLamportsBigInt = Buffer.from(data).readBigUInt64LE(4); - const amountLamports = Number(amountLamportsBigInt); - - expect(amounts[i]).toEqual(amountLamports); - } - }); - - test("can sign multiple versioned Solana transfers against production", async () => { - if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { - // This test requires an env var to be set - throw new Error( - "This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" - ); - } - - const client = new TurnkeyClient( - { baseUrl: "https://api.turnkey.com" }, - new ApiKeyStamper({ - apiPublicKey, - apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, - }) - ); - - const signer = new TurnkeySigner({ - organizationId, - client, - }); + }, + { + configName: "@turnkey/sdk-browser client", + signer: new TurnkeySigner({ + organizationId, + client: turnkeyServerClient.apiClient(), + }), + }, + ].forEach(async (signerConfig) => { + describe(`using ${signerConfig.configName}`, () => { + test("can sign a Solana transfer against production", async () => { + const transferTransaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: new PublicKey(turnkeySolAddress), + // Destination doesn't matter, we set it to the Turnkey war chest! + toPubkey: new PublicKey(TKHQ_WARCHEST), + lamports: 10000, + }) + ); - const fromKey = new PublicKey(turnkeySolAddress); - const numTxs = 3; - const transactions = new Array(); - const amounts = new Array(); - - for (let i = 0; i < numTxs; i++) { - const amount = Math.floor(Math.random() * 100); // random amount - amounts.push(amount); - - const instructions = [ - SystemProgram.transfer({ - fromPubkey: fromKey, - // Destination doesn't matter, we set it to the Turnkey war chest! - toPubkey: new PublicKey(TKHQ_WARCHEST), - lamports: amount, - }), - ]; - - // Create v0 compatible message - const messageV0 = new TransactionMessage({ - payerKey: fromKey, // Doesn't really matter since we're not going to broadcast this transaction! - recentBlockhash: DEFAULT_BLOCK_HASH, - instructions, - }).compileToV0Message(); - - const transaction = new VersionedTransaction(messageV0); - - // version transactions are initialized with a default signature - expect(transaction.signatures.length).toBe(1); - expect(transaction.signatures[0]).toEqual(DEFAULT_SIGNATURE); - - transactions.push(transaction); - } - - const signedTransactions = await signer.signAllTransactions( - transactions, - turnkeySolAddress - ); - expect(signedTransactions.length).toBe(numTxs); - - for (let i = 0; i < signedTransactions.length; i++) { - const tx = signedTransactions[i] as VersionedTransaction; - - // After signing the version transaction, the default signature is replaced with the new one - expect(tx.signatures.length).toBe(1); - expect(tx.signatures[0]).not.toEqual(DEFAULT_SIGNATURE); - - // Verify the signature itself - const isValidSignature = nacl.sign.detached.verify( - tx.message.serialize(), - tx.signatures[0] as Uint8Array, - bs58.decode(turnkeySolAddress) - ); - expect(isValidSignature).toBeTruthy(); - - // Ensure it's a simple, native transfer - expect(tx.message.compiledInstructions.length).toEqual(1); - - const programIdIndex = - tx.message.compiledInstructions[0]!.programIdIndex!; - const keys = tx.message.getAccountKeys(); - const programId = keys.staticAccountKeys[programIdIndex]; - const data = tx.message.compiledInstructions[0]!.data!; - - expect(programId).toEqual(SystemProgram.programId); - expect(data[0]).toEqual(2); - - // Convert raw data to lamports, then to whole SOL units - const amountLamportsBigInt = Buffer.from(data).readBigUInt64LE(4); - const amountLamports = Number(amountLamportsBigInt); - - expect(amounts[i]).toEqual(amountLamports); - } - }); - - test("can sign a message with a Solana account", async () => { - if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) { - // This test requires an env var to be set - throw new Error( - "This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set" - ); - } - - const client = new TurnkeyClient( - { baseUrl: "https://api.turnkey.com" }, - new ApiKeyStamper({ - apiPublicKey, - apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY, - }) - ); - - const signer = new TurnkeySigner({ - organizationId, - client, + // But if we don't set this the call to "serializeMessage fails." + transferTransaction.recentBlockhash = DEFAULT_BLOCK_HASH; + transferTransaction.feePayer = new PublicKey(turnkeySolAddress); + + expect(transferTransaction.signatures.length).toBe(0); + await signerConfig.signer.addSignature( + transferTransaction, + turnkeySolAddress + ); + expect(transferTransaction.signatures.length).toBe(1); + + const isValidSignature = nacl.sign.detached.verify( + transferTransaction.serializeMessage(), + transferTransaction.signature as Uint8Array, + bs58.decode(turnkeySolAddress) + ); + expect(isValidSignature).toBeTruthy(); + }); + + test("can sign a versioned Solana transfer against production", async () => { + const fromKey = new PublicKey(turnkeySolAddress); + + const instructions = [ + SystemProgram.transfer({ + fromPubkey: fromKey, + // Destination doesn't matter, we set it to the Turnkey war chest! + toPubkey: new PublicKey(TKHQ_WARCHEST), + lamports: 10, + }), + ]; + + // create v0 compatible message + const messageV0 = new TransactionMessage({ + payerKey: fromKey, + // Doesn't really matter since we're not going to broadcast this transaction! + recentBlockhash: DEFAULT_BLOCK_HASH, + instructions, + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + // version transactions are initialized with a default signature + expect(transaction.signatures.length).toBe(1); + expect(transaction.signatures[0]).toEqual(DEFAULT_SIGNATURE); + + await signerConfig.signer.addSignature(transaction, turnkeySolAddress); + + // after signing the version transaction, the default signature is replaced with the new one + expect(transaction.signatures.length).toBe(1); + expect(transaction.signatures[0]).not.toEqual(DEFAULT_SIGNATURE); + + const isValidSignature = nacl.sign.detached.verify( + transaction.message.serialize(), + transaction.signatures[0] as Uint8Array, + bs58.decode(turnkeySolAddress) + ); + expect(isValidSignature).toBeTruthy(); + }); + + test("can sign multiple Solana transfers against production", async () => { + const numTxs = 3; + const transactions = new Array(); + const amounts = new Array(); + + for (let i = 0; i < numTxs; i++) { + const amount = Math.floor(Math.random() * 100); // random amount + amounts.push(amount); + + const transferTransaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: new PublicKey(turnkeySolAddress), + // Destination doesn't matter, we set it to the Turnkey war chest! + toPubkey: new PublicKey(TKHQ_WARCHEST), + lamports: amount, + }) + ); + + expect(transferTransaction.signatures.length).toBe(0); + + // Doesn't really matter since we're not going to broadcast this transaction! + // But if we don't set this the call to "serializeMessage fails." + transferTransaction.recentBlockhash = DEFAULT_BLOCK_HASH; + transferTransaction.feePayer = new PublicKey(turnkeySolAddress); + + transactions.push(transferTransaction); + } + + const signedTransactions = + await signerConfig.signer.signAllTransactions( + transactions, + turnkeySolAddress + ); + expect(signedTransactions.length).toBe(numTxs); + + for (let i = 0; i < signedTransactions.length; i++) { + const tx = signedTransactions[i] as Transaction; + + // Verify the signature itself + const isValidSignature = nacl.sign.detached.verify( + tx.serializeMessage(), + tx.signature as Uint8Array, + bs58.decode(turnkeySolAddress) + ); + expect(isValidSignature).toBeTruthy(); + + // Ensure it's a simple, native transfer + expect(tx.instructions.length).toEqual(1); + + const programId = tx.instructions[0]!.programId!; + const data = tx.instructions[0]!.data!; + + expect(programId).toEqual(SystemProgram.programId); + expect(data[0]).toEqual(2); + + // Convert raw data to lamports, then to whole SOL units + const amountLamportsBigInt = Buffer.from(data).readBigUInt64LE(4); + const amountLamports = Number(amountLamportsBigInt); + + expect(amounts[i]).toEqual(amountLamports); + } + }); + + test("can sign multiple versioned Solana transfers against production", async () => { + const fromKey = new PublicKey(turnkeySolAddress); + const numTxs = 3; + const transactions = new Array(); + const amounts = new Array(); + + for (let i = 0; i < numTxs; i++) { + const amount = Math.floor(Math.random() * 100); // random amount + amounts.push(amount); + + const instructions = [ + SystemProgram.transfer({ + fromPubkey: fromKey, + // Destination doesn't matter, we set it to the Turnkey war chest! + toPubkey: new PublicKey(TKHQ_WARCHEST), + lamports: amount, + }), + ]; + + // Create v0 compatible message + const messageV0 = new TransactionMessage({ + payerKey: fromKey, + // Doesn't really matter since we're not going to broadcast this transaction! + recentBlockhash: DEFAULT_BLOCK_HASH, + instructions, + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + // version transactions are initialized with a default signature + expect(transaction.signatures.length).toBe(1); + expect(transaction.signatures[0]).toEqual(DEFAULT_SIGNATURE); + + transactions.push(transaction); + } + + const signedTransactions = + await signerConfig.signer.signAllTransactions( + transactions, + turnkeySolAddress + ); + expect(signedTransactions.length).toBe(numTxs); + + for (let i = 0; i < signedTransactions.length; i++) { + const tx = signedTransactions[i] as VersionedTransaction; + + // After signing the version transaction, the default signature is replaced with the new one + expect(tx.signatures.length).toBe(1); + expect(tx.signatures[0]).not.toEqual(DEFAULT_SIGNATURE); + + // Verify the signature itself + const isValidSignature = nacl.sign.detached.verify( + tx.message.serialize(), + tx.signatures[0] as Uint8Array, + bs58.decode(turnkeySolAddress) + ); + expect(isValidSignature).toBeTruthy(); + + // Ensure it's a simple, native transfer + expect(tx.message.compiledInstructions.length).toEqual(1); + + const programIdIndex = + tx.message.compiledInstructions[0]!.programIdIndex!; + const keys = tx.message.getAccountKeys(); + const programId = keys.staticAccountKeys[programIdIndex]; + const data = tx.message.compiledInstructions[0]!.data!; + + expect(programId).toEqual(SystemProgram.programId); + expect(data[0]).toEqual(2); + + // Convert raw data to lamports, then to whole SOL units + const amountLamportsBigInt = Buffer.from(data).readBigUInt64LE(4); + const amountLamports = Number(amountLamportsBigInt); + + expect(amounts[i]).toEqual(amountLamports); + } + }); + + test("can sign a message with a Solana account", async () => { + const message = "Hello world!"; + const messageAsUint8Array = Buffer.from(message); + + const signature = await signerConfig.signer.signMessage( + messageAsUint8Array, + turnkeySolAddress + ); + + const isValidSignature = nacl.sign.detached.verify( + messageAsUint8Array, + signature, + bs58.decode(turnkeySolAddress) + ); + expect(isValidSignature).toBeTruthy(); + }); }); - - const message = "Hello world!"; - const messageAsUint8Array = Buffer.from(message); - - const signature = await signer.signMessage( - messageAsUint8Array, - turnkeySolAddress - ); - - const isValidSignature = nacl.sign.detached.verify( - messageAsUint8Array, - signature, - bs58.decode(turnkeySolAddress) - ); - expect(isValidSignature).toBeTruthy(); }); }); From c3429542b38825ef3d13e304fab7755f50ce7a7d Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 26 Aug 2024 17:03:20 -0400 Subject: [PATCH 3/5] add changeset --- .changeset/nervous-bananas-know.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nervous-bananas-know.md diff --git a/.changeset/nervous-bananas-know.md b/.changeset/nervous-bananas-know.md new file mode 100644 index 000000000..a7ac2e6bd --- /dev/null +++ b/.changeset/nervous-bananas-know.md @@ -0,0 +1,5 @@ +--- +"@turnkey/solana": minor +--- + +Add compatibility with @turnkey/sdk-server and @turnkey/sdk-browser From 6aa7865a55305258752206a94481b4e64e549cd8 Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 26 Aug 2024 17:11:41 -0400 Subject: [PATCH 4/5] update changelogs --- .changeset/nervous-bananas-know.md | 5 ----- examples/with-wallet-stamper/CHANGELOG.md | 7 +++++++ examples/with-wallet-stamper/package.json | 2 +- packages/solana/CHANGELOG.md | 6 ++++++ packages/solana/package.json | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 .changeset/nervous-bananas-know.md diff --git a/.changeset/nervous-bananas-know.md b/.changeset/nervous-bananas-know.md deleted file mode 100644 index a7ac2e6bd..000000000 --- a/.changeset/nervous-bananas-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@turnkey/solana": minor ---- - -Add compatibility with @turnkey/sdk-server and @turnkey/sdk-browser diff --git a/examples/with-wallet-stamper/CHANGELOG.md b/examples/with-wallet-stamper/CHANGELOG.md index 622bb0c7f..b0b45bdb0 100644 --- a/examples/with-wallet-stamper/CHANGELOG.md +++ b/examples/with-wallet-stamper/CHANGELOG.md @@ -1,5 +1,12 @@ # with-wallet-stamper +## 0.1.5 + +### Patch Changes + +- Updated dependencies [c342954] + - @turnkey/solana@0.4.0 + ## 0.1.4 ### Patch Changes diff --git a/examples/with-wallet-stamper/package.json b/examples/with-wallet-stamper/package.json index 3ef3b81af..f50859d63 100644 --- a/examples/with-wallet-stamper/package.json +++ b/examples/with-wallet-stamper/package.json @@ -1,6 +1,6 @@ { "name": "with-wallet-stamper", - "version": "0.1.4", + "version": "0.1.5", "private": true, "type": "module", "scripts": { diff --git a/packages/solana/CHANGELOG.md b/packages/solana/CHANGELOG.md index a5e1129a5..6d778fe63 100644 --- a/packages/solana/CHANGELOG.md +++ b/packages/solana/CHANGELOG.md @@ -1,5 +1,11 @@ # @turnkey/solana +## 0.4.0 + +### Minor Changes + +- c342954: Add compatibility with @turnkey/sdk-server and @turnkey/sdk-browser + ## 0.3.10 ### Patch Changes diff --git a/packages/solana/package.json b/packages/solana/package.json index 07d57c609..2cd6e86aa 100644 --- a/packages/solana/package.json +++ b/packages/solana/package.json @@ -1,6 +1,6 @@ { "name": "@turnkey/solana", - "version": "0.3.10", + "version": "0.4.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "exports": { From b6e99beea04b2ee6a732e88fe03ed853f1de534a Mon Sep 17 00:00:00 2001 From: Andrew Min Date: Mon, 26 Aug 2024 17:25:29 -0400 Subject: [PATCH 5/5] update solana example --- examples/with-solana/package.json | 1 + examples/with-solana/src/advanced.ts | 20 +++---- examples/with-solana/src/index.ts | 20 +++---- examples/with-solana/src/tokenTransfer.ts | 21 ++++--- .../src/utils/createSolanaWallet.ts | 58 ++++++------------- pnpm-lock.yaml | 3 + 6 files changed, 50 insertions(+), 73 deletions(-) diff --git a/examples/with-solana/package.json b/examples/with-solana/package.json index 7a4181ca7..96661f1e3 100644 --- a/examples/with-solana/package.json +++ b/examples/with-solana/package.json @@ -16,6 +16,7 @@ "@solana/web3.js": "^1.88.1", "@turnkey/api-key-stamper": "workspace:*", "@turnkey/http": "workspace:*", + "@turnkey/sdk-server": "workspace:*", "@turnkey/solana": "workspace:*", "bs58": "^5.0.0", "dotenv": "^16.0.3", diff --git a/examples/with-solana/src/advanced.ts b/examples/with-solana/src/advanced.ts index ae99432d9..2e024a1dd 100644 --- a/examples/with-solana/src/advanced.ts +++ b/examples/with-solana/src/advanced.ts @@ -14,8 +14,7 @@ import { TransactionMessage, } from "@solana/web3.js"; -import { TurnkeyClient } from "@turnkey/http"; -import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { Turnkey } from "@turnkey/sdk-server"; import { TurnkeySigner } from "@turnkey/solana"; import { createNewSolanaWallet, solanaNetwork } from "./utils"; @@ -27,22 +26,21 @@ async function main() { const connection = solanaNetwork.connect(); - const turnkeyClient = new TurnkeyClient( - { baseUrl: process.env.BASE_URL! }, - new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }) - ); + const turnkeyClient = new Turnkey({ + apiBaseUrl: process.env.BASE_URL!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: organizationId, + }); const turnkeySigner = new TurnkeySigner({ organizationId, - client: turnkeyClient, + client: turnkeyClient.apiClient(), }); let solAddress = process.env.SOLANA_ADDRESS!; if (!solAddress) { - solAddress = await createNewSolanaWallet(turnkeyClient, organizationId); + solAddress = await createNewSolanaWallet(turnkeyClient.apiClient()); console.log(`\nYour new Solana address: "${solAddress}"`); } else { console.log(`\nUsing existing Solana address from ENV: "${solAddress}"`); diff --git a/examples/with-solana/src/index.ts b/examples/with-solana/src/index.ts index 40c34191b..06e529d1c 100644 --- a/examples/with-solana/src/index.ts +++ b/examples/with-solana/src/index.ts @@ -5,8 +5,7 @@ import bs58 from "bs58"; import { input, confirm } from "@inquirer/prompts"; import type { Transaction } from "@solana/web3.js"; -import { TurnkeyClient } from "@turnkey/http"; -import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { Turnkey } from "@turnkey/sdk-server"; import { TurnkeySigner } from "@turnkey/solana"; const TURNKEY_WAR_CHEST = "tkhqC9QX2gkqJtUFk2QKhBmQfFyyqZXSpr73VFRi35C"; @@ -27,22 +26,21 @@ async function main() { const connection = solanaNetwork.connect(); - const turnkeyClient = new TurnkeyClient( - { baseUrl: process.env.BASE_URL! }, - new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }) - ); + const turnkeyClient = new Turnkey({ + apiBaseUrl: process.env.BASE_URL!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: organizationId, + }); const turnkeySigner = new TurnkeySigner({ organizationId, - client: turnkeyClient, + client: turnkeyClient.apiClient(), }); let solAddress = process.env.SOLANA_ADDRESS!; if (!solAddress) { - solAddress = await createNewSolanaWallet(turnkeyClient, organizationId); + solAddress = await createNewSolanaWallet(turnkeyClient.apiClient()); console.log(`\nYour new Solana address: "${solAddress}"`); } else { console.log(`\nUsing existing Solana address from ENV: "${solAddress}"`); diff --git a/examples/with-solana/src/tokenTransfer.ts b/examples/with-solana/src/tokenTransfer.ts index 38052189b..0b02b068d 100644 --- a/examples/with-solana/src/tokenTransfer.ts +++ b/examples/with-solana/src/tokenTransfer.ts @@ -8,8 +8,7 @@ import { confirm } from "@inquirer/prompts"; import { PublicKey } from "@solana/web3.js"; import { getAccount, getAssociatedTokenAddress } from "@solana/spl-token"; -import { TurnkeyClient } from "@turnkey/http"; -import { ApiKeyStamper } from "@turnkey/api-key-stamper"; +import { Turnkey } from "@turnkey/sdk-server"; import { TurnkeySigner } from "@turnkey/solana"; import { @@ -26,22 +25,22 @@ async function main() { const turnkeyWarchest = new PublicKey(TURNKEY_WAR_CHEST); const organizationId = process.env.ORGANIZATION_ID!; const connection = solanaNetwork.connect(); - const turnkeyClient = new TurnkeyClient( - { baseUrl: process.env.BASE_URL! }, - new ApiKeyStamper({ - apiPublicKey: process.env.API_PUBLIC_KEY!, - apiPrivateKey: process.env.API_PRIVATE_KEY!, - }) - ); + + const turnkeyClient = new Turnkey({ + apiBaseUrl: process.env.BASE_URL!, + apiPublicKey: process.env.API_PUBLIC_KEY!, + apiPrivateKey: process.env.API_PRIVATE_KEY!, + defaultOrganizationId: organizationId, + }); const turnkeySigner = new TurnkeySigner({ organizationId, - client: turnkeyClient, + client: turnkeyClient.apiClient(), }); let solAddress = process.env.SOLANA_ADDRESS!; if (!solAddress) { - solAddress = await createNewSolanaWallet(turnkeyClient, organizationId); + solAddress = await createNewSolanaWallet(turnkeyClient.apiClient()); console.log(`\nYour new Solana address: "${solAddress}"`); } else { console.log(`\nUsing existing Solana address from ENV: "${solAddress}"`); diff --git a/examples/with-solana/src/utils/createSolanaWallet.ts b/examples/with-solana/src/utils/createSolanaWallet.ts index 6329e06c8..6d9d382cd 100644 --- a/examples/with-solana/src/utils/createSolanaWallet.ts +++ b/examples/with-solana/src/utils/createSolanaWallet.ts @@ -1,57 +1,35 @@ -import { - TurnkeyActivityError, - TurnkeyClient, - createActivityPoller, -} from "@turnkey/http"; +import { TurnkeyActivityError } from "@turnkey/http"; +import type { TurnkeyApiClient } from "@turnkey/sdk-server"; import * as crypto from "crypto"; -export async function createNewSolanaWallet( - client: TurnkeyClient, - turnkeyOrganizationId: string -) { +export async function createNewSolanaWallet(client: TurnkeyApiClient) { console.log("creating a new Solana wallet in your Turnkey organization...\n"); const walletName = `Solana Wallet ${crypto.randomBytes(2).toString("hex")}`; try { - const activityPoller = createActivityPoller({ - client: client, - requestFn: client.createWallet, + const response = await client.createWallet({ + walletName, + accounts: [ + { + pathFormat: "PATH_FORMAT_BIP32", + // https://github.com/satoshilabs/slips/blob/master/slip-0044.md + path: "m/44'/501'/0'/0'", + curve: "CURVE_ED25519", + addressFormat: "ADDRESS_FORMAT_SOLANA", + }, + ], }); - const completedActivity = await activityPoller({ - type: "ACTIVITY_TYPE_CREATE_WALLET", - organizationId: turnkeyOrganizationId, - parameters: { - walletName, - accounts: [ - { - pathFormat: "PATH_FORMAT_BIP32", - // https://github.com/satoshilabs/slips/blob/master/slip-0044.md - path: "m/44'/501'/0'/0'", - curve: "CURVE_ED25519", - addressFormat: "ADDRESS_FORMAT_SOLANA", - }, - ], - }, - timestampMs: String(Date.now()), // millisecond timestamp - }); - - const walletId = completedActivity.result.createWalletResult?.walletId; + const walletId = response.walletId; if (!walletId) { - console.error( - "activity doesn't contain a valid wallet ID", - completedActivity - ); + console.error("response doesn't contain a valid wallet ID"); process.exit(1); } - const address = completedActivity.result.createWalletResult?.addresses[0]; + const address = response.addresses[0]; if (!address) { - console.error( - "activity result doesn't contain a valid address", - completedActivity - ); + console.error("response doesn't contain a valid address"); process.exit(1); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f92445f04..5a9e8603f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -636,6 +636,9 @@ importers: '@turnkey/http': specifier: workspace:* version: link:../../packages/http + '@turnkey/sdk-server': + specifier: workspace:* + version: link:../../packages/sdk-server '@turnkey/solana': specifier: workspace:* version: link:../../packages/solana