Skip to content
Open
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
13 changes: 6 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
"build": "tsup"
},
"dependencies": {
"@noble/curves": "^1.9.1",
"@scure/base": "^1.2.6",
"siwe": "^2.3.2",
"tweetnacl": "^1.0.3",
"viem": "^2.46.2",
"zod": "^3.24.2"
},
Expand Down
10 changes: 8 additions & 2 deletions core/src/solana.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import nacl from 'tweetnacl'
import { ed25519 } from '@noble/curves/ed25519'
import { base58 } from '@scure/base'
import type { CompleteAgentkitInfo } from './types'

Expand Down Expand Up @@ -48,7 +48,13 @@ export function formatSIWSMessage(info: CompleteAgentkitInfo, address: string):

export function verifySolanaSignature(message: string, signature: Uint8Array, publicKey: Uint8Array): boolean {
const messageBytes = new TextEncoder().encode(message)
return nacl.sign.detached.verify(messageBytes, signature, publicKey)
try {
return ed25519.verify(signature, messageBytes, publicKey, { zip215: false })
} catch {
// @noble/curves throws on malformed inputs (wrong length, non-canonical points);
// tweetnacl returned false. Preserve the boolean contract for callers.
return false
}
}

export function decodeBase58(encoded: string): Uint8Array {
Expand Down
54 changes: 54 additions & 0 deletions core/tests/solana.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'bun:test'
import { verifySolanaSignature } from '../src/solana'

function fromHex(hex: string): Uint8Array {
const out = new Uint8Array(hex.length / 2)
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
}
return out
}

// Fixture generated with tweetnacl@1.0.3 from a deterministic seed.
// Guards against regressions if the underlying ed25519 implementation changes.
const FIXTURE = {
message:
'example.com wants you to sign in with your Solana account:\nABC\n\nURI: https://example.com\nVersion: 1\nChain ID: mainnet\nNonce: abcd1234\nIssued At: 2026-05-13T00:00:00.000Z',
publicKey: fromHex('6b80f36fa38d2942de85ff15bff2c62704c9fc9a4c1174a2dd5b8e1cd91f4326'),
signature: fromHex(
'3cbd22d2a13d291ce5a52c379f9fc2f6de624fccdaf6de7e3b0d4a7168b06c111f6c6d82e81590c13503c374c966c448c8ef374613c822ba983f61d9dc463a01'
),
}

describe('verifySolanaSignature', () => {
it('accepts a valid signature produced by tweetnacl', () => {
expect(verifySolanaSignature(FIXTURE.message, FIXTURE.signature, FIXTURE.publicKey)).toBe(true)
})

it('rejects a tampered signature', () => {
const tampered = new Uint8Array(FIXTURE.signature)
tampered[0] ^= 0x01
expect(verifySolanaSignature(FIXTURE.message, tampered, FIXTURE.publicKey)).toBe(false)
})

it('rejects a tampered message', () => {
expect(verifySolanaSignature(FIXTURE.message + ' ', FIXTURE.signature, FIXTURE.publicKey)).toBe(false)
})

it('rejects a signature checked against the wrong public key', () => {
const wrongKey = new Uint8Array(FIXTURE.publicKey)
wrongKey[0] ^= 0x01
expect(verifySolanaSignature(FIXTURE.message, FIXTURE.signature, wrongKey)).toBe(false)
})

it('returns false for a malformed signature instead of throwing', () => {
const wrongLength = new Uint8Array(10)
expect(verifySolanaSignature(FIXTURE.message, wrongLength, FIXTURE.publicKey)).toBe(false)

const allZero = new Uint8Array(64)
expect(verifySolanaSignature(FIXTURE.message, allZero, FIXTURE.publicKey)).toBe(false)

const allOnes = new Uint8Array(64).fill(0xff)
expect(verifySolanaSignature(FIXTURE.message, allOnes, FIXTURE.publicKey)).toBe(false)
})
})