diff --git a/mobile_app/app/dev/contract-spike.tsx b/mobile_app/app/dev/contract-spike.tsx new file mode 100644 index 0000000..ef76e11 --- /dev/null +++ b/mobile_app/app/dev/contract-spike.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useState } from 'react'; +import { ScrollView, StyleSheet, Text } from 'react-native'; + +import { DepthButton } from '@/components/primitives'; +import { solanaConnection } from '@/src/infrastructure/network/connection'; +import { + SPIKE_FIXTURES, + SPIKE_PROGRAM_ID, + simulateCosignedTransfer, + type SpikeSimResult, +} from '@/src/services/contractSpike'; +import { fontFamily, fontSize, spacing, useTheme } from '@/theme'; + +/** + * SPIKE — proves the app can build a valid anonbeta1 execute_cosigned_transfer + * and the deployed devnet program accepts it (simulation, no signatures, no + * funds moved). Dev builds only; not reachable from any production surface. + */ +export default function ContractSpikeScreen() { + const { colors } = useTheme(); + const [busy, setBusy] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const run = useCallback(async () => { + if (busy) return; + setBusy(true); + setError(null); + setResult(null); + try { + setResult(await simulateCosignedTransfer(solanaConnection)); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + }, [busy]); + + const mono = { fontFamily: fontFamily.mono, fontSize: fontSize.xs }; + + return ( + + anonbeta1 settlement spike + + Builds an execute_cosigned_transfer against the live devnet program and + simulates it (no signatures, nothing moves). The real path co-signs with + a beacon operator — proven end-to-end from the workstation harness. + + + PROGRAM + {SPIKE_PROGRAM_ID.toBase58()} + BEACON OPERATOR (devnet fixture) + {SPIKE_FIXTURES.operator.toBase58()} + + + + {error && ( + {error} + )} + + {result && ( + <> + + {result.ok ? 'SIMULATION OK — program accepted the instruction' : `SIMULATION FAILED: ${result.err}`} + + BEACON PDA + {result.beaconPda} + SETTLEMENT PDA (fresh) + {result.settlementPda} + PROGRAM LOGS + {result.logs.slice(-12).map((l, i) => ( + + {l} + + ))} + + )} + + ); +} + +const S = StyleSheet.create({ + content: { padding: spacing[6], gap: spacing[3], paddingBottom: spacing[9] }, + title: { fontFamily: fontFamily.sansBold, fontSize: fontSize['2xl'] }, + body: { fontFamily: fontFamily.sans, fontSize: fontSize.sm, lineHeight: 20 }, + label: { + fontFamily: fontFamily.sansSb, + fontSize: fontSize.xs, + letterSpacing: 1.2, + marginTop: spacing[3], + textTransform: 'uppercase', + }, + button: { marginTop: spacing[5] }, +}); diff --git a/mobile_app/app/dev/index.tsx b/mobile_app/app/dev/index.tsx index ac963e9..e60ff2c 100644 --- a/mobile_app/app/dev/index.tsx +++ b/mobile_app/app/dev/index.tsx @@ -1,5 +1,5 @@ import { Feather } from '@expo/vector-icons'; -import { router } from 'expo-router'; +import { type Href, router } from 'expo-router'; import React, { useState } from 'react'; import { Pressable, ScrollView, StyleSheet, Text } from 'react-native'; @@ -30,6 +30,7 @@ export default function DevIndexScreen() { SCREENS router.push('/dev/pigeon-loader')} /> + router.push('/dev/contract-spike' as Href)} /> FIRST-RUN STATE diff --git a/mobile_app/src/services/contractSpike.ts b/mobile_app/src/services/contractSpike.ts new file mode 100644 index 0000000..d98c9f9 --- /dev/null +++ b/mobile_app/src/services/contractSpike.ts @@ -0,0 +1,137 @@ +// SPIKE — anonbeta1 execute_cosigned_transfer from the app (devnet, __DEV__ only). +// +// The live program (anon7uu8…, devnet) settles a co-signed SPL transfer: +// sender + beacon operator both sign; the beacon takes share_bps and the +// settlement PDA records it. This module proves the APP can construct that +// transaction correctly — verified against the real chain via simulation +// (sigVerify off), since the co-sign needs the beacon operator's key, which +// lives with the beacon, not in this app. +// +// Machine-side twin: contract/scripts/cosign-transfer-spike.mjs executed the +// full co-signed path on devnet (see overnight3 report for the signature). +// Fixtures below come from that run. NOT wired into the live send path. + +import { Buffer } from 'buffer'; +import { + Connection, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, +} from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'; + +export const SPIKE_PROGRAM_ID = new PublicKey('anon7uu8UtVoFgS8GCSfw2RqyphJhkN3xEjgPwznYDe'); + +// anchor discriminator for execute_cosigned_transfer (target/idl/anonbeta1.json) +const DISC = Buffer.from([77, 199, 16, 225, 200, 193, 184, 138]); + +// Devnet fixtures established by cosign-transfer-spike.mjs on 2026-06-10: +// a binding_verified beacon (Arcium roundtrip) + a throwaway 6-dec mint with +// funded sender/beacon ATAs. Public keys only. +export const SPIKE_FIXTURES = { + operator: new PublicKey('96pAGQK9Fa4dD17oDH9qDw6n38aNteLEEKKakNgsYUWw'), + mint: new PublicKey('2enp3pWDd6eGFn2a9KcnjAypMBdDfHkjaHccK2nymzDy'), + fundedSender: new PublicKey('CaQAKBcwf7G5vXeu2RNuNGJafnJ8724Uj4wv9ivfxfQA'), +} as const; + +export function deriveBeaconPda(operator: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('beacon'), operator.toBuffer()], + SPIKE_PROGRAM_ID, + )[0]; +} + +export function deriveSettlementPda(sender: PublicKey, settlementId: Uint8Array): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from('settlement'), sender.toBuffer(), settlementId], + SPIKE_PROGRAM_ID, + )[0]; +} + +export type CosignedTransferParams = Readonly<{ + sender: PublicKey; + operator: PublicKey; + mint: PublicKey; + recipientTokenAccount: PublicKey; + settlementId: Uint8Array; // 32 bytes, non-zero + amount: bigint; + beaconShareBps: number; +}>; + +export function buildExecuteCosignedTransferIx(p: CosignedTransferParams): TransactionInstruction { + if (p.settlementId.length !== 32) throw new Error('settlementId must be 32 bytes'); + const beacon = deriveBeaconPda(p.operator); + const settlement = deriveSettlementPda(p.sender, p.settlementId); + const senderAta = getAssociatedTokenAddressSync(p.mint, p.sender); + const beaconAta = getAssociatedTokenAddressSync(p.mint, p.operator); + + const amount = Buffer.alloc(8); + amount.writeBigUInt64LE(p.amount); + const share = Buffer.alloc(2); + share.writeUInt16LE(p.beaconShareBps); + const data = Buffer.concat([DISC, Buffer.from(p.settlementId), amount, share]); + + return new TransactionInstruction({ + programId: SPIKE_PROGRAM_ID, + data, + keys: [ + { pubkey: p.sender, isSigner: true, isWritable: true }, + { pubkey: p.operator, isSigner: true, isWritable: false }, + { pubkey: beacon, isSigner: false, isWritable: true }, + { pubkey: p.operator, isSigner: false, isWritable: false }, + { pubkey: settlement, isSigner: false, isWritable: true }, + { pubkey: p.mint, isSigner: false, isWritable: false }, + { pubkey: senderAta, isSigner: false, isWritable: true }, + { pubkey: p.recipientTokenAccount, isSigner: false, isWritable: true }, + { pubkey: beaconAta, isSigner: false, isWritable: true }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + }); +} + +export type SpikeSimResult = Readonly<{ + ok: boolean; + beaconPda: string; + settlementPda: string; + logs: readonly string[]; + err: string | null; +}>; + +// Builds a fresh self-pay settlement (sender ATA doubles as the recipient leg — +// the program leaves recipient ATA ownership to the signed body) and simulates +// it against devnet without signatures. Proves discriminator, arg encoding, +// PDA derivation, and account ordering against the deployed program. +export async function simulateCosignedTransfer(connection: Connection): Promise { + const { operator, mint, fundedSender } = SPIKE_FIXTURES; + const settlementId = new Uint8Array(32); + // Expo provides getRandomValues via expo-crypto polyfill at app start. + globalThis.crypto.getRandomValues(settlementId); + + const ix = buildExecuteCosignedTransferIx({ + sender: fundedSender, + operator, + mint, + recipientTokenAccount: getAssociatedTokenAddressSync(mint, fundedSender), + settlementId, + amount: 250_000n, + beaconShareBps: 200, + }); + + const tx = new Transaction().add(ix); + tx.feePayer = fundedSender; + const { blockhash } = await connection.getLatestBlockhash('confirmed'); + tx.recentBlockhash = blockhash; + + // Legacy-transaction simulate path runs with sigVerify=false — exactly what + // we need: validate the instruction against the live program, no keys. + const sim = await connection.simulateTransaction(tx); + return { + ok: sim.value.err === null, + beaconPda: deriveBeaconPda(operator).toBase58(), + settlementPda: deriveSettlementPda(fundedSender, settlementId).toBase58(), + logs: sim.value.logs ?? [], + err: sim.value.err === null ? null : JSON.stringify(sim.value.err), + }; +}