Skip to content

Commit 8061386

Browse files
committed
Merge pull-request #202
2 parents 157db4b + 3232aae commit 8061386

File tree

5 files changed

+93
-13
lines changed

5 files changed

+93
-13
lines changed

.changeset/hungry-otters-destroy.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@turnkey/solana": minor
3+
---
4+
5+
implemented signMessage on the Solana TurnkeySigner

packages/solana/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252
"@turnkey/http": "workspace:*"
5353
},
5454
"devDependencies": {
55-
"@turnkey/api-key-stamper": "workspace:*"
55+
"@turnkey/api-key-stamper": "workspace:*",
56+
"bs58": "^5.0.0",
57+
"tweetnacl": "^1.0.3"
5658
},
5759
"engines": {
5860
"node": ">=18.0.0"

packages/solana/src/__tests__/index-test.ts

+45-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ import { TurnkeySigner } from "../";
33
import { TurnkeyClient } from "@turnkey/http";
44
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
55
import { PublicKey, SystemProgram, Transaction } from "@solana/web3.js";
6+
import nacl from "tweetnacl";
7+
import bs58 from "bs58";
68

79
describe("TurnkeySigner", () => {
10+
const organizationId = "4456e4c2-e8b5-4b93-a0cf-1dfae265c12c";
11+
const apiPublicKey =
12+
"025d374c674fc389c761462f3c59c0acabdcb3a17c599d9e62e5fe78fe984cfbeb";
13+
const turnkeySolAddress = "D8P541wwnertZTgDT14kYJPoFT2eHUFqjTgPxMK5qatM";
814
test("can sign a Solana transfer against production", async () => {
915
if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) {
1016
// This test requires an env var to be set
@@ -16,19 +22,16 @@ describe("TurnkeySigner", () => {
1622
const client = new TurnkeyClient(
1723
{ baseUrl: "https://api.turnkey.com" },
1824
new ApiKeyStamper({
19-
apiPublicKey:
20-
"025d374c674fc389c761462f3c59c0acabdcb3a17c599d9e62e5fe78fe984cfbeb",
25+
apiPublicKey,
2126
apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY,
2227
})
2328
);
2429

2530
const signer = new TurnkeySigner({
26-
organizationId: "4456e4c2-e8b5-4b93-a0cf-1dfae265c12c",
31+
organizationId,
2732
client,
2833
});
2934

30-
const turnkeySolAddress = "D8P541wwnertZTgDT14kYJPoFT2eHUFqjTgPxMK5qatM";
31-
3235
const transferTransaction = new Transaction().add(
3336
SystemProgram.transfer({
3437
fromPubkey: new PublicKey(turnkeySolAddress),
@@ -48,4 +51,41 @@ describe("TurnkeySigner", () => {
4851
await signer.addSignature(transferTransaction, turnkeySolAddress);
4952
expect(transferTransaction.signatures.length).toBe(1);
5053
});
54+
55+
test("can sign a message with a Solana account", async () => {
56+
if (!process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY) {
57+
// This test requires an env var to be set
58+
throw new Error(
59+
"This test requires SOLANA_TEST_ORG_API_PRIVATE_KEY to be set"
60+
);
61+
}
62+
63+
const client = new TurnkeyClient(
64+
{ baseUrl: "https://api.turnkey.com" },
65+
new ApiKeyStamper({
66+
apiPublicKey,
67+
apiPrivateKey: process.env.SOLANA_TEST_ORG_API_PRIVATE_KEY,
68+
})
69+
);
70+
71+
const signer = new TurnkeySigner({
72+
organizationId,
73+
client,
74+
});
75+
76+
const message = "Hello world!";
77+
const messageAsUint8Array = Buffer.from(message);
78+
79+
const signature = await signer.signMessage(
80+
messageAsUint8Array,
81+
turnkeySolAddress
82+
);
83+
84+
const isValidSignature = nacl.sign.detached.verify(
85+
messageAsUint8Array,
86+
signature,
87+
bs58.decode(turnkeySolAddress)
88+
);
89+
expect(isValidSignature).toBeTruthy();
90+
});
5191
});

packages/solana/src/index.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,44 @@ export class TurnkeySigner {
2020
const fromKey = new PublicKey(fromAddress);
2121
const messageToSign = tx.serializeMessage();
2222

23+
const signRawPayloadResult = await this.signRawPayload(
24+
messageToSign.toString("hex"),
25+
fromAddress
26+
);
27+
28+
const signature = `${signRawPayloadResult.signRawPayloadResult?.r}${signRawPayloadResult.signRawPayloadResult?.s}`;
29+
30+
tx.addSignature(fromKey, Buffer.from(signature, "hex"));
31+
}
32+
33+
/**
34+
* This function takes a message and returns it after being signed with Turnkey
35+
*
36+
* @param message The message to sign (Uint8Array)
37+
* @param fromAddress Solana address (base58 encoded)
38+
*/
39+
public async signMessage(
40+
message: Uint8Array,
41+
fromAddress: string
42+
): Promise<Uint8Array> {
43+
const signRawPayloadResult = await this.signRawPayload(
44+
Buffer.from(message).toString("hex"),
45+
fromAddress
46+
);
47+
return Buffer.from(
48+
`${signRawPayloadResult.signRawPayloadResult?.r}${signRawPayloadResult.signRawPayloadResult?.s}`,
49+
"hex"
50+
);
51+
}
52+
53+
private async signRawPayload(payload: string, signWith: string) {
2354
const response = await this.client.signRawPayload({
2455
type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
2556
organizationId: this.organizationId,
2657
timestampMs: String(Date.now()),
2758
parameters: {
28-
signWith: fromAddress,
29-
payload: messageToSign.toString("hex"),
59+
signWith,
60+
payload,
3061
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
3162
// Note: unlike ECDSA, EdDSA's API does not support signing raw digests (see RFC 8032).
3263
// Turnkey's signer requires an explicit value to be passed here to minimize ambiguity.
@@ -45,8 +76,6 @@ export class TurnkeySigner {
4576
});
4677
}
4778

48-
const signature = `${result.signRawPayloadResult?.r}${result.signRawPayloadResult?.s}`;
49-
50-
tx.addSignature(fromKey, Buffer.from(signature, "hex"));
79+
return result;
5180
}
5281
}

pnpm-lock.yaml

+6-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)