Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@anthropic-ai/sdk": "^0.39.0",
"@mariozechner/pi-agent-core": "^0.66.1",
"@mariozechner/pi-ai": "^0.66.1",
"@noble/ciphers": "^2.1.1",
"@sinclair/typebox": "^0.34.49",
"@sipher/sdk": "workspace:*",
"better-sqlite3": "^12.8.0",
Expand Down
42 changes: 38 additions & 4 deletions packages/agent/src/routes/vault-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const MINT_LABELS: Record<string, string> = {
[USDT_MINT.toBase58()]: 'USDT',
}

// Known decimals for common mints (avoids extra RPC call)
const KNOWN_DECIMALS: Record<string, number> = {
[WSOL_MINT.toBase58()]: 9,
[USDC_MINT.toBase58()]: 6,
[USDT_MINT.toBase58()]: 6,
}

interface TokenBalance {
mint: string
symbol: string
Expand All @@ -32,6 +39,7 @@ vaultRouter.get('/', async (req: Request, res: Response) => {
const connection = createConnection(network)

let solBalance = 0
let balanceStatus: 'ok' | 'unavailable' = 'ok'
const tokens: TokenBalance[] = []

try {
Expand All @@ -46,18 +54,43 @@ vaultRouter.get('/', async (req: Request, res: Response) => {
programId: TOKEN_PROGRAM_ID,
})

// Collect unknown mints to batch-fetch decimals
const unknownMints: PublicKey[] = []
const tokenEntries: { mint: PublicKey; mintStr: string; rawAmount: bigint }[] = []

for (const { account } of tokenAccounts.value) {
// SPL token account data layout: mint(32) + owner(32) + amount(8) + ...
const data = account.data
const mint = new PublicKey(data.subarray(0, 32))
const mintStr = mint.toBase58()
const rawAmount = data.readBigUInt64LE(64)

// Skip zero-balance accounts and wrapped SOL (shown as native SOL above)
if (rawAmount === 0n || mint.equals(WSOL_MINT)) continue

tokenEntries.push({ mint, mintStr, rawAmount })
if (!(mintStr in KNOWN_DECIMALS)) {
unknownMints.push(mint)
}
}

// Batch-fetch decimals for unknown mints (SPL mint layout: decimals at offset 44)
const fetchedDecimals: Record<string, number> = {}
if (unknownMints.length > 0) {
try {
const mintAccounts = await connection.getMultipleAccountsInfo(unknownMints)
for (let i = 0; i < unknownMints.length; i++) {
const info = mintAccounts[i]
if (info?.data && info.data.length >= 45) {
fetchedDecimals[unknownMints[i].toBase58()] = info.data[44]
}
}
} catch {
// Non-fatal — fall back to 9 for unknown mints
}
}

for (const { mintStr, rawAmount } of tokenEntries) {
const symbol = MINT_LABELS[mintStr] ?? mintStr.slice(0, 8) + '...'
const decimals = mint.equals(USDC_MINT) || mint.equals(USDT_MINT) ? 6 : 9
const decimals = KNOWN_DECIMALS[mintStr] ?? fetchedDecimals[mintStr] ?? 9
const uiAmount = Number(rawAmount) / 10 ** decimals

tokens.push({
Expand All @@ -69,7 +102,7 @@ vaultRouter.get('/', async (req: Request, res: Response) => {
})
}
} catch (err) {
// RPC failures should not block the entire response — return what we have
balanceStatus = 'unavailable'
console.warn('[vault] balance fetch failed:', err instanceof Error ? err.message : err)
}

Expand All @@ -81,6 +114,7 @@ vaultRouter.get('/', async (req: Request, res: Response) => {
balances: {
sol: solBalance,
tokens,
status: balanceStatus,
},
activity,
})
Expand Down
30 changes: 18 additions & 12 deletions packages/agent/src/tools/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
commit,
} from '@sip-protocol/sdk'
import { sha256 } from '@noble/hashes/sha256'
import { xchacha20poly1305 } from '@noble/ciphers/chacha'
import { randomBytes } from '@noble/ciphers/webcrypto'
import {
createConnection,
buildPrivateSendTx,
Expand Down Expand Up @@ -204,22 +206,26 @@ export async function executeSend(params: SendParams): Promise<SendToolResult> {
viewingKeyHash = new Uint8Array(32).fill(0)
}

// Encrypt amount + blinding factor for recipient to verify commitment.
// Layout: [8 bytes amount LE] || [32 bytes blinding] XOR-masked with viewing key hash.
// Encrypt amount + blinding factor with XChaCha20-Poly1305 (AEAD).
// Key = viewing key hash (32 bytes), nonce = random 24 bytes.
// Layout: [24 bytes nonce] || [ciphertext + 16 bytes tag]
// Plaintext: [8 bytes amount LE] || [32 bytes blinding]
let encryptedAmount: Uint8Array
if (isStealthMetaAddress) {
const amountLeBytes = bigintToLeBytes(BigInt(amountBase))
const blindingBytes = hexToBytes(blinding)
const payload = new Uint8Array(amountLeBytes.length + blindingBytes.length)
payload.set(amountLeBytes, 0)
payload.set(blindingBytes, amountLeBytes.length)
// Generate 64 bytes of keystream from viewing key hash
const mask1 = sha256(viewingKeyHash)
const mask2 = sha256(mask1)
encryptedAmount = new Uint8Array(payload.length)
for (let i = 0; i < payload.length; i++) {
encryptedAmount[i] = payload[i] ^ (i < 32 ? mask1[i] : mask2[i - 32])
}
const plaintext = new Uint8Array(amountLeBytes.length + blindingBytes.length)
plaintext.set(amountLeBytes, 0)
plaintext.set(blindingBytes, amountLeBytes.length)

const nonce = randomBytes(24)
const cipher = xchacha20poly1305(viewingKeyHash, nonce)
const ciphertext = cipher.encrypt(plaintext)

// Prepend nonce so recipient can decrypt
encryptedAmount = new Uint8Array(nonce.length + ciphertext.length)
encryptedAmount.set(nonce, 0)
encryptedAmount.set(ciphertext, nonce.length)
} else {
encryptedAmount = new Uint8Array(0)
}
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

5 changes: 1 addition & 4 deletions src/services/jupiter-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,11 @@ export async function buildSwapTransaction(params: SwapTransactionParams): Promi

// ─── Utility ────────────────────────────────────────────────────────────────

/** Token label lookup (informational — Jupiter supports all SPL tokens). */
export function getSupportedTokens(): typeof SUPPORTED_TOKENS {
return SUPPORTED_TOKENS
}

export function isTokenSupported(_mint: string): boolean {
return true
}

export function resetJupiterProvider(): void {
quoteCache.clear()
}
Loading