From 08cb6097249a5ada49fdc04ebb019bb5244a00ac Mon Sep 17 00:00:00 2001 From: Asem- Abdelhady Date: Thu, 9 Apr 2026 13:17:03 +0200 Subject: [PATCH 1/4] feat: fill solana to evm intents --- src/lib/abi/polymer.json | 392 +++++++++++++++++++++++++ src/lib/libraries/flowProgress.ts | 77 ++++- src/lib/libraries/solanaEscrowLib.ts | 4 +- src/lib/libraries/solanaFinaliseLib.ts | 193 ++++++++++++ src/lib/libraries/solanaValidateLib.ts | 272 +++++++++++++++++ src/lib/libraries/solver.ts | 8 +- src/lib/screens/FillIntent.svelte | 47 ++- src/lib/screens/Finalise.svelte | 179 +++++++++-- src/lib/screens/ReceiveMessage.svelte | 264 +++++++++++++---- src/lib/state.svelte.ts | 3 +- src/lib/utils/reviveOrderBigInts.ts | 68 +++++ src/routes/+page.svelte | 9 +- tests/unit/reviveOrderBigInts.test.ts | 100 +++++++ 13 files changed, 1531 insertions(+), 85 deletions(-) create mode 100644 src/lib/abi/polymer.json create mode 100644 src/lib/libraries/solanaFinaliseLib.ts create mode 100644 src/lib/libraries/solanaValidateLib.ts create mode 100644 src/lib/utils/reviveOrderBigInts.ts create mode 100644 tests/unit/reviveOrderBigInts.test.ts diff --git a/src/lib/abi/polymer.json b/src/lib/abi/polymer.json new file mode 100644 index 0000000..7deaa82 --- /dev/null +++ b/src/lib/abi/polymer.json @@ -0,0 +1,392 @@ +{ + "address": "C2rAFLS6xQ78t18rK5s9madY9fztbhTaHwShgYtzonk7", + "metadata": { + "name": "polymer", + "version": "0.0.0", + "spec": "0.1.0" + }, + "instructions": [ + { + "name": "initialize", + "docs": [ + "Initializes the `Polymer` program. Creates \"Polymer\" PDA and sets the `polymer_prover_id`.", + "\"Polymer\" PDA is used sign and attest for the messages received." + ], + "discriminator": [175, 175, 109, 31, 13, 152, 155, 237], + "accounts": [ + { + "name": "deployer", + "writable": true, + "signer": true + }, + { + "name": "chain_id" + }, + { + "name": "oracle_polymer", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "polymer_prover_id", + "type": "pubkey" + } + ] + }, + { + "name": "receive", + "docs": [ + "Receive attestation proofs of output settlers from a remote oracle and register them", + "locally by creating attestation accounts." + ], + "discriminator": [86, 17, 255, 171, 17, 17, 187, 219], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "polymer_prover_program" + }, + { + "name": "chain_mapping" + }, + { + "name": "cache_account", + "docs": ["This is the same PDA that was automatically created during load_proof"], + "writable": true + }, + { + "name": "internal", + "writable": true + }, + { + "name": "result_account", + "writable": true + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "proofs", + "type": { + "vec": "bytes" + } + } + ] + }, + { + "name": "set_chain_mapping", + "docs": [ + "Configure the `protocol_chain_id` that corresponds to the given Polymer chain identifier.", + "chain_id is the real chain identifier used by the oracle implementation.", + "Can only be called by the owner of the PolymerOracle.", + "Each mapping may only be set once." + ], + "discriminator": [174, 145, 44, 171, 203, 101, 230, 130], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "chain_mapping", + "writable": true + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "protocol_chain_id", + "type": "u128" + }, + { + "name": "chain_id", + "type": "u128" + } + ] + }, + { + "name": "submit", + "docs": [ + "Checks the fill descriptions and the local attestations.", + "Emits log in the form of `Prove: program: {0x...}, Application: {0x...}, PayloadHash: {0x...}`." + ], + "discriminator": [88, 166, 102, 181, 162, 127, 170, 48], + "accounts": [ + { + "name": "submitter", + "writable": true, + "signer": true + }, + { + "name": "oracle_polymer", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [112, 111, 108, 121, 109, 101, 114] + } + ] + } + }, + { + "name": "intents_protocol_program", + "address": "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz" + } + ], + "args": [ + { + "name": "source", + "type": "pubkey" + }, + { + "name": "fill_descriptions", + "type": { + "vec": "bytes" + } + } + ] + } + ], + "accounts": [ + { + "name": "ChainId", + "discriminator": [203, 146, 201, 161, 230, 71, 157, 218] + }, + { + "name": "ChainMapping", + "discriminator": [81, 78, 77, 209, 44, 206, 85, 169] + }, + { + "name": "OraclePolymer", + "discriminator": [30, 219, 153, 176, 126, 163, 80, 138] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidChainId", + "msg": "Invalid chain id" + }, + { + "code": 6001, + "name": "InvalidProof", + "msg": "Invalid proof" + }, + { + "code": 6002, + "name": "Unauthorized", + "msg": "Unauthorized mapping creation" + }, + { + "code": 6003, + "name": "CouldNotDecodeSolver", + "msg": "Could not decode solver" + }, + { + "code": 6004, + "name": "CouldNotDecodeTimestamp", + "msg": "Could not decode timestamp" + }, + { + "code": 6005, + "name": "CouldNotDecodeMandateOutputFieldsTuple", + "msg": "Could not decode mandate output fields tuple" + }, + { + "code": 6006, + "name": "CouldNotDecodeOracle", + "msg": "Could not decode oracle" + }, + { + "code": 6007, + "name": "CouldNotDecodeSettler", + "msg": "Could not decode settler" + }, + { + "code": 6008, + "name": "CouldNotDecodeChainId", + "msg": "Could not decode chain id" + }, + { + "code": 6009, + "name": "CouldNotDecodeToken", + "msg": "Could not decode token" + }, + { + "code": 6010, + "name": "CouldNotDecodeAmount", + "msg": "Could not decode amount" + }, + { + "code": 6011, + "name": "CouldNotDecodeRecipient", + "msg": "Could not decode recipient" + }, + { + "code": 6012, + "name": "CouldNotDecodeCallbackData", + "msg": "Could not decode callback data" + }, + { + "code": 6013, + "name": "CouldNotDecodeContext", + "msg": "Could not decode context" + }, + { + "code": 6014, + "name": "CouldNotDecodeFinalAmount", + "msg": "Could not decode final amount" + }, + { + "code": 6015, + "name": "CouldNotConvertU256ToU64", + "msg": "Could not convert u128 to u64" + }, + { + "code": 6016, + "name": "CouldNotConvertU256ToU128", + "msg": "Could not convert u256 to u128" + }, + { + "code": 6017, + "name": "CouldNotConvertU256ToU32", + "msg": "Could not convert u256 to u32" + }, + { + "code": 6018, + "name": "NoAttestationsProvided", + "msg": "No attestations provided" + }, + { + "code": 6019, + "name": "NotCorrectEncodedTypes", + "msg": "Not correct encoded types" + } + ], + "types": [ + { + "name": "ChainId", + "docs": ["Type used to store the chain id of the chain."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "ChainMapping", + "docs": [ + "Type used to map a protocol chain id to a real chain id.", + "The protocol chain id is given as seed." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + }, + { + "name": "OraclePolymer", + "docs": [ + "The PDA to be created that will sign the attestations", + "`polymer_prover_id` is the program id of the polymer prover program.", + "`owner` is the owner of the PolymerOracle. The only address to create mappings.", + "`bump` is the bump of this account to be reused." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "polymer_prover_id", + "type": "pubkey" + }, + { + "name": "owner", + "type": "pubkey" + }, + { + "name": "chain_id", + "type": "u128" + }, + { + "name": "bump", + "type": "u8" + } + ] + } + } + ] +} diff --git a/src/lib/libraries/flowProgress.ts b/src/lib/libraries/flowProgress.ts index 9c89091..eab1b6c 100644 --- a/src/lib/libraries/flowProgress.ts +++ b/src/lib/libraries/flowProgress.ts @@ -5,19 +5,23 @@ import { INPUT_SETTLER_ESCROW_LIFI, MULTICHAIN_INPUT_SETTLER_COMPACT, MULTICHAIN_INPUT_SETTLER_ESCROW, - getClient + getClient, + getSolanaConnection, + isSolanaChain } from "$lib/config"; import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; import { POLYMER_ORACLE_ABI } from "$lib/abi/polymeroracle"; import { SETTLER_ESCROW_ABI } from "$lib/abi/escrow"; import { COMPACT_ABI } from "$lib/abi/compact"; -import { hashStruct, keccak256 } from "viem"; +import { hashStruct, keccak256, parseEventLogs } from "viem"; import { compactTypes } from "@lifi/intent"; import { getOutputHash, encodeMandateOutput } from "@lifi/intent"; import { addressToBytes32, bytes32ToAddress } from "@lifi/intent"; import { containerToIntent } from "$lib/utils/intent"; import { getOrFetchRpc } from "$lib/libraries/rpcCache"; -import type { MandateOutput, OrderContainer } from "@lifi/intent"; +import { deriveAttestationPda } from "$lib/libraries/solanaValidateLib"; +import { deriveOrderContextPda } from "$lib/libraries/solanaFinaliseLib"; +import type { MandateOutput, OrderContainer, StandardSolana } from "@lifi/intent"; import store from "$lib/state.svelte"; const PROGRESS_TTL_MS = 30_000; @@ -93,6 +97,55 @@ async function isOutputValidatedOnChain( .catch((error) => console.warn("saveTransactionReceipt error", error)); } + // Solana→EVM: check attestation PDA existence instead of calling EVM isProven + if (isSolanaChain(inputChain)) { + return getOrFetchRpc( + `progress:solana-proven:${orderId}:${inputChain.toString()}:${outputKey}:${fillTransactionHash}`, + async () => { + const logs = parseEventLogs({ + abi: COIN_FILLER_ABI, + eventName: "OutputFilled", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + logs: (receipt as any).logs + }); + const expectedHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: output + }); + const matchingLog = logs.find((log) => { + const logOutputHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: log.args.output + }); + return logOutputHash === expectedHash; + }); + if (!matchingLog) return false; + const solverBytes32 = matchingLog.args.solver as `0x${string}`; + const fillTimestamp = + typeof matchingLog.args.timestamp === "number" + ? matchingLog.args.timestamp + : Number(matchingLog.args.timestamp); + const attestationPda = await deriveAttestationPda({ + evmChainId: output.chainId, + output, + proofOutput: matchingLog.args.output as MandateOutput, + orderId, + fillTimestamp, + solverBytes32, + emittingContract: matchingLog.address as `0x${string}` + }); + const { PublicKey } = await import("@solana/web3.js"); + const info = await getSolanaConnection(inputChain).getAccountInfo( + new PublicKey(attestationPda) + ); + return info !== null; + }, + { ttlMs: PROGRESS_TTL_MS } + ); + } + const block = await getOrFetchRpc( `progress:block:${output.chainId.toString()}:${receipt.blockHash}`, async () => { @@ -127,10 +180,26 @@ async function isOutputValidatedOnChain( async function isInputChainFinalised(chainId: bigint, container: OrderContainer) { const { order, inputSettler } = container; - const inputChainClient = getClient(chainId); const intent = containerToIntent(container); const orderId = intent.orderId(); + // Solana→EVM: order_context PDA is closed after finalise + if (isSolanaChain(chainId)) { + return getOrFetchRpc( + `progress:finalised:solana:${orderId}:${chainId.toString()}`, + async () => { + const { PublicKey } = await import("@solana/web3.js"); + const conn = getSolanaConnection(chainId); + const orderContextPda = await deriveOrderContextPda(order as StandardSolana); + const info = await conn.getAccountInfo(new PublicKey(orderContextPda)); + return info === null; // null = closed = finalised + }, + { ttlMs: PROGRESS_TTL_MS } + ); + } + + const inputChainClient = getClient(chainId); + if ( inputSettler === INPUT_SETTLER_ESCROW_LIFI || inputSettler === MULTICHAIN_INPUT_SETTLER_ESCROW diff --git a/src/lib/libraries/solanaEscrowLib.ts b/src/lib/libraries/solanaEscrowLib.ts index 96cd567..e26ac6e 100644 --- a/src/lib/libraries/solanaEscrowLib.ts +++ b/src/lib/libraries/solanaEscrowLib.ts @@ -85,7 +85,9 @@ export async function openSolanaEscrow(params: { // Extract input token from StandardSolana. // Solana token IDs are full 32-byte public keys stored as bigint — do NOT use idToToken() // which strips the first 12 bytes (EVM-only helper that returns 20-byte addresses). - const tokenIdHex = order.inputs[0][0].toString(16).padStart(64, "0"); + const tokenIdHex = BigInt(order.inputs[0][0] as bigint | string | number) + .toString(16) + .padStart(64, "0"); const inputMint = new PublicKey(Buffer.from(tokenIdHex, "hex")); const inputAmount = new BN(order.inputs[0][1].toString()); diff --git a/src/lib/libraries/solanaFinaliseLib.ts b/src/lib/libraries/solanaFinaliseLib.ts new file mode 100644 index 0000000..3fcb6a9 --- /dev/null +++ b/src/lib/libraries/solanaFinaliseLib.ts @@ -0,0 +1,193 @@ +import { keccak256 } from "viem"; +import idl from "../abi/input_settler_escrow.json"; +import { + SOLANA_INPUT_SETTLER_ESCROW, + SOLANA_INTENTS_PROTOCOL, + SOLANA_POLYMER_ORACLE +} from "../config"; +import { borshEncodeSolanaOrder } from "@lifi/intent"; +import type { MandateOutput, StandardSolana } from "@lifi/intent"; +import type { SignerWalletAdapter } from "@solana/wallet-adapter-base"; +import type { Connection } from "@solana/web3.js"; + +/** Convert a 0x-prefixed hex string (32 bytes) to a number[] */ +function hexToBytes32(hex: `0x${string}`): number[] { + return Array.from(Buffer.from(hex.slice(2), "hex")); +} + +/** Convert a bigint to a 32-byte big-endian number[] */ +function bigintToBeBytes32(n: bigint | string | number): number[] { + return Array.from(Buffer.from(BigInt(n).toString(16).padStart(64, "0"), "hex")); +} + +/** + * Derive the order_context PDA for a StandardSolana order. + * orderId = keccak256(borsh(order)); seeds = [b"order_context", orderId] + * Used to check if the order has been finalised — PDA closed means finalised. + */ +export async function deriveOrderContextPda(order: StandardSolana): Promise { + const { PublicKey } = await import("@solana/web3.js"); + const inputSettlerProgramId = new PublicKey(SOLANA_INPUT_SETTLER_ESCROW); + + const encoded = borshEncodeSolanaOrder(order); + const orderIdHex = keccak256(encoded); + const orderId = Buffer.from(orderIdHex.slice(2), "hex"); + + const [orderContextPda] = PublicKey.findProgramAddressSync( + [Buffer.from("order_context"), orderId], + inputSettlerProgramId + ); + + return orderContextPda.toBase58(); +} + +/** + * Call input_settler_escrow.finalise() on Solana to release escrowed tokens to the solver. + * + * @param order The StandardSolana order that was previously opened + * @param solveParams One entry per output: solver = 32-byte pubkey array, timestamp = EVM fill block timestamp + * @param attestationPdas Base58 pubkeys of attestation PDAs (one per output, from submitProofToSolanaOracle) + * @param solanaPublicKey Base58-encoded Solana solver public key (signer + token destination) + * @param walletAdapter Connected Solana wallet adapter + * @param connection Solana Connection instance + */ +export async function finaliseSolanaEscrow(params: { + order: StandardSolana; + solveParams: { solver: number[]; timestamp: number }[]; + attestationPdas: string[]; + solanaPublicKey: string; + walletAdapter: SignerWalletAdapter; + connection: Connection; +}): Promise { + const { AnchorProvider, BN, Program } = await import("@coral-xyz/anchor"); + const { PublicKey, SystemProgram, Transaction } = await import("@solana/web3.js"); + const { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } = + await import("@solana/spl-token"); + + const { order, solveParams, attestationPdas, solanaPublicKey, walletAdapter, connection } = + params; + + const solverPubkey = new PublicKey(solanaPublicKey); + const inputSettlerProgramId = new PublicKey(SOLANA_INPUT_SETTLER_ESCROW); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const intentsProtocolId = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + const anchorWallet = { + publicKey: solverPubkey, + signTransaction: < + T extends + | import("@solana/web3.js").Transaction + | import("@solana/web3.js").VersionedTransaction + >( + tx: T + ) => walletAdapter.signTransaction(tx), + signAllTransactions: < + T extends + | import("@solana/web3.js").Transaction + | import("@solana/web3.js").VersionedTransaction + >( + txs: T[] + ) => walletAdapter.signAllTransactions(txs) + }; + // `idl` is typed as a plain JSON object; cast to any so Anchor's Program generic accepts it. + // `anchorWallet` is cast to any because Anchor's internal AnchorWallet type may differ across + // peer-dependency versions of @solana/web3.js. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedIdl = idl as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(connection, anchorWallet as any, { commitment: "confirmed" }); + const program = new Program(typedIdl, provider); + + const [polymerOraclePda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + + // StandardSolana stores the token as a bigint (32-byte public key) + const [tokenBigInt, inputAmount] = order.inputs[0]; + const tokenIdHex = BigInt(tokenBigInt as bigint | string | number) + .toString(16) + .padStart(64, "0"); + const inputMint = new PublicKey(Buffer.from(tokenIdHex, "hex")); + const userPubkey = new PublicKey(Buffer.from(order.user.slice(2), "hex")); + + const anchorOrder = { + user: userPubkey, + nonce: new BN(order.nonce.toString()), + originChainId: new BN(order.originChainId.toString()), + expires: order.expires, + fillDeadline: order.fillDeadline, + inputOracle: polymerOraclePda, + input: { token: inputMint, amount: new BN(inputAmount.toString()) }, + outputs: order.outputs.map((o: MandateOutput) => ({ + oracle: hexToBytes32(o.oracle), + settler: hexToBytes32(o.settler), + chainId: bigintToBeBytes32(o.chainId), + token: hexToBytes32(o.token), + amount: bigintToBeBytes32(o.amount), + recipient: hexToBytes32(o.recipient), + callbackData: + o.callbackData === "0x" ? Buffer.alloc(0) : Buffer.from(o.callbackData.slice(2), "hex"), + context: o.context === "0x" ? Buffer.alloc(0) : Buffer.from(o.context.slice(2), "hex") + })) + }; + + // Compute orderId = keccak256(borsh(order)) — uses the canonical @lifi/intent encoder + const encoded = borshEncodeSolanaOrder(order); + const orderIdHex = keccak256(encoded); + const orderId = Buffer.from(orderIdHex.slice(2), "hex"); + + // Derive PDAs + const [inputSettlerEscrowPda] = PublicKey.findProgramAddressSync( + [Buffer.from("input_settler_escrow")], + inputSettlerProgramId + ); + const [orderContext] = PublicKey.findProgramAddressSync( + [Buffer.from("order_context"), orderId], + inputSettlerProgramId + ); + + // ATAs: destination = solver, orderContext = escrow holding the input tokens + const destinationTokenAccount = getAssociatedTokenAddressSync(inputMint, solverPubkey, false); + const orderPdaTokenAccount = getAssociatedTokenAddressSync(inputMint, orderContext, true); + + const remainingAccounts = attestationPdas.map((pda) => ({ + pubkey: new PublicKey(pda), + isWritable: false, + isSigner: false + })); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ix = await program.methods + .finalise(anchorOrder as any, solveParams as any) + .accounts({ + solver: solverPubkey, + inputSettlerEscrow: inputSettlerEscrowPda, + user: userPubkey, + destination: solverPubkey, + destinationTokenAccount, + orderContext, + orderPdaTokenAccount, + mint: inputMint, + intentsProtocolProgram: intentsProtocolId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId + } as any) + .remainingAccounts(remainingAccounts) + .instruction(); + + // The on-chain finalise flow closes escrow/state accounts to `user`, so `user` + // must be writable even though the generated IDL may mark it readonly. + for (const key of ix.keys) { + if (key.pubkey.equals(userPubkey)) key.isWritable = true; + } + + const tx = new Transaction().add(ix); + tx.feePayer = solverPubkey; + tx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash; + + const signature = await provider.sendAndConfirm(tx, [], { commitment: "confirmed" }); + + return signature; +} diff --git a/src/lib/libraries/solanaValidateLib.ts b/src/lib/libraries/solanaValidateLib.ts new file mode 100644 index 0000000..3f5978c --- /dev/null +++ b/src/lib/libraries/solanaValidateLib.ts @@ -0,0 +1,272 @@ +import axios from "axios"; +import { keccak256 } from "viem"; +import idl from "../abi/polymer.json"; +import { SOLANA_INTENTS_PROTOCOL, SOLANA_POLYMER_ORACLE } from "../config"; +import type { MandateOutput } from "@lifi/intent"; +import type { SignerWalletAdapter } from "@solana/wallet-adapter-base"; +import type { Connection } from "@solana/web3.js"; + +const POLYMER_PROVER_PROGRAM = "CdvSq48QUukYuMczgZAVNZrwcHNshBdtqrjW26sQiGPs"; + +/** Convert a bigint (or number) to a 16-byte little-endian Buffer (u128 LE) */ +function u128ToLeBytes(n: bigint | number): Buffer { + const v = BigInt(n); + const buf = Buffer.alloc(16); + buf.writeBigUInt64LE(v & 0xffffffffffffffffn, 0); + buf.writeBigUInt64LE(v >> 64n, 8); + return buf; +} + +function normalizeBytes32Hex(value: `0x${string}`): Buffer { + return Buffer.from(value.slice(2), "hex"); +} + +function normalizeEvmIdentifier( + value: `0x${string}` | undefined, + fallbackBytes32: `0x${string}` +): Buffer { + if (!value) return normalizeBytes32Hex(fallbackBytes32); + const hex = value.slice(2); + if (hex.length === 40) return Buffer.from(hex.padStart(64, "0"), "hex"); + if (hex.length === 64) return Buffer.from(hex, "hex"); + throw new Error(`Invalid EVM identifier length: ${value.length}`); +} + +/** Encode the common payload for a MandateOutput */ +export function encodeCommonPayload(output: MandateOutput): Buffer { + const token = Buffer.from(output.token.slice(2), "hex"); + const amountHex = BigInt(output.amount).toString(16).padStart(64, "0"); + const amount = Buffer.from(amountHex, "hex"); + const recipient = Buffer.from(output.recipient.slice(2), "hex"); + const callbackData = + output.callbackData === "0x" + ? Buffer.alloc(0) + : Buffer.from(output.callbackData.slice(2), "hex"); + const context = + output.context === "0x" ? Buffer.alloc(0) : Buffer.from(output.context.slice(2), "hex"); + const callLen = Buffer.alloc(2); + callLen.writeUInt16BE(callbackData.length, 0); + const ctxLen = Buffer.alloc(2); + ctxLen.writeUInt16BE(context.length, 0); + return Buffer.concat([token, amount, recipient, callLen, callbackData, ctxLen, context]); +} + +/** Encode fill description: solver(32) || orderId(32) || timestamp(4,BE) || commonPayload */ +export function encodeFillDescription( + solverBytes32: `0x${string}`, + orderId: `0x${string}`, + timestamp: number, + commonPayload: Buffer +): Buffer { + const solver = Buffer.from(solverBytes32.slice(2), "hex"); + const orderIdBytes = Buffer.from(orderId.slice(2), "hex"); + const ts = Buffer.alloc(4); + ts.writeUInt32BE(timestamp >>> 0, 0); + return Buffer.concat([solver, orderIdBytes, ts, commonPayload]); +} + +/** + * Derive the attestation PDA for a given fill. + * Seeds: [b"attestation", oracle_polymer_pda, evmChainId_le16, output.oracle, output.settler, payloadHash] + * Program: SOLANA_INTENTS_PROTOCOL + */ +export async function deriveAttestationPda(params: { + evmChainId: bigint; + output: MandateOutput; + proofOutput?: MandateOutput; + orderId: `0x${string}`; + fillTimestamp: number; + solverBytes32: `0x${string}`; + emittingContract?: `0x${string}`; +}): Promise { + const { PublicKey } = await import("@solana/web3.js"); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const intentsProtocol = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + const [oraclePolymerPda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + + const payloadOutput = params.proofOutput ?? params.output; + const chainIdSeed = u128ToLeBytes(params.evmChainId); + const commonPayload = encodeCommonPayload(payloadOutput); + const fillDesc = encodeFillDescription( + params.solverBytes32, + params.orderId, + params.fillTimestamp, + commonPayload + ); + const payloadHash = Buffer.from(keccak256(fillDesc).slice(2), "hex"); + const source = normalizeBytes32Hex(payloadOutput.oracle); + const application = normalizeEvmIdentifier(params.emittingContract, payloadOutput.settler); + + const [attestationPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("attestation"), + oraclePolymerPda.toBuffer(), + chainIdSeed, + source, + application, + payloadHash + ], + intentsProtocol + ); + + return attestationPda.toBase58(); +} + +/** + * Submit a Polymer proof to the Solana oracle_polymer.receive() instruction. + * Fetches the proof from the /polymer API (hex-encoded) then calls oracle_polymer.receive(). + */ +export async function submitProofToSolanaOracle(params: { + evmChainId: bigint; + output: MandateOutput; + proofOutput?: MandateOutput; + orderId: `0x${string}`; + fillTimestamp: number; + solverBytes32: `0x${string}`; + emittingContract?: `0x${string}`; + fillBlockNumber: number; + globalLogIndex: number; + mainnet: boolean; + solanaPublicKey: string; + walletAdapter: SignerWalletAdapter; + connection: Connection; +}): Promise { + const { AnchorProvider, Program } = await import("@coral-xyz/anchor"); + const { PublicKey, SystemProgram, ComputeBudgetProgram } = await import("@solana/web3.js"); + + const signerPubkey = new PublicKey(params.solanaPublicKey); + const polymerOracleProgram = new PublicKey(SOLANA_POLYMER_ORACLE); + const polymerProverProgramId = new PublicKey(POLYMER_PROVER_PROGRAM); + const intentsProtocolId = new PublicKey(SOLANA_INTENTS_PROTOCOL); + + // Fetch Polymer proof via /polymer route (returns hex-encoded bytes) + let proof: string | undefined; + let polymerIndex: number | undefined; + for (const waitMs of [0, 2000, 4000, 8000, 16000]) { + if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs)); + const response = await axios.post( + "/polymer", + { + srcChainId: Number(params.evmChainId), + srcBlockNumber: params.fillBlockNumber, + globalLogIndex: params.globalLogIndex, + polymerIndex, + mainnet: params.mainnet + }, + { timeout: 15_000 } + ); + const dat = response.data as { proof: string | undefined; polymerIndex: number }; + polymerIndex = dat.polymerIndex; + if (dat.proof) { + proof = dat.proof; + break; + } + } + if (!proof) { + throw new Error("Polymer proof unavailable. Try again after the fill attestation is indexed."); + } + + // Derive PDAs + const [oraclePolymerPda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerOracleProgram + ); + const chainIdSeed = u128ToLeBytes(params.evmChainId); + + const [chainMapping] = PublicKey.findProgramAddressSync( + [Buffer.from("chain_mapping"), oraclePolymerPda.toBuffer(), chainIdSeed], + intentsProtocolId + ); + const [cacheAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("cache"), signerPubkey.toBuffer()], + polymerProverProgramId + ); + const [internalAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("internal")], + polymerProverProgramId + ); + const [resultAccount] = PublicKey.findProgramAddressSync( + [Buffer.from("result"), signerPubkey.toBuffer()], + polymerProverProgramId + ); + + // Attestation PDA (goes into remaining_accounts as writable) + const payloadOutput = params.proofOutput ?? params.output; + const commonPayload = encodeCommonPayload(payloadOutput); + const fillDesc = encodeFillDescription( + params.solverBytes32, + params.orderId, + params.fillTimestamp, + commonPayload + ); + const payloadHash = Buffer.from(keccak256(fillDesc).slice(2), "hex"); + const source = normalizeBytes32Hex(payloadOutput.oracle); + const emittingContractApplication = normalizeEvmIdentifier( + params.emittingContract, + payloadOutput.settler + ); + + const [attestationPda] = PublicKey.findProgramAddressSync( + [ + Buffer.from("attestation"), + oraclePolymerPda.toBuffer(), + chainIdSeed, + source, + emittingContractApplication, + payloadHash + ], + intentsProtocolId + ); + + // Build Anchor program + const anchorWallet = { + publicKey: signerPubkey, + signTransaction: < + T extends + | import("@solana/web3.js").Transaction + | import("@solana/web3.js").VersionedTransaction + >( + tx: T + ) => params.walletAdapter.signTransaction(tx), + signAllTransactions: < + T extends + | import("@solana/web3.js").Transaction + | import("@solana/web3.js").VersionedTransaction + >( + txs: T[] + ) => params.walletAdapter.signAllTransactions(txs) + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typedIdl = idl as any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(params.connection, anchorWallet as any, { + commitment: "confirmed" + }); + const program = new Program(typedIdl, provider); + + const proofBytes = Buffer.from(proof, "hex"); + const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + return await program.methods + .receive([proofBytes] as any) + .accounts({ + signer: signerPubkey, + oraclePolymer: oraclePolymerPda, + polymerProverProgram: polymerProverProgramId, + chainMapping, + cacheAccount, + internal: internalAccount, + resultAccount, + intentsProtocolProgram: intentsProtocolId, + systemProgram: SystemProgram.programId + } as any) + .remainingAccounts([{ pubkey: attestationPda, isWritable: true, isSigner: false }]) + .preInstructions([computeIx]) + .rpc({ commitment: "confirmed" }); + /* eslint-enable @typescript-eslint/no-explicit-any */ +} diff --git a/src/lib/libraries/solver.ts b/src/lib/libraries/solver.ts index 38e393d..b3c64a3 100644 --- a/src/lib/libraries/solver.ts +++ b/src/lib/libraries/solver.ts @@ -53,6 +53,7 @@ export class Solver { args: { orderContainer: OrderContainer; outputs: MandateOutput[]; + solverBytes32?: `0x${string}`; // override default addressToBytes32(account()) — used for Solana→EVM fills }, opts: { preHook?: (chainId: number) => Promise; @@ -64,7 +65,8 @@ export class Solver { const { preHook, postHook, account } = opts; const { orderContainer: { order, inputSettler }, - outputs + outputs, + solverBytes32 } = args; const orderId = containerToIntent(args.orderContainer).orderId(); @@ -122,7 +124,7 @@ export class Solver { value, abi: COIN_FILLER_ABI, functionName: "fillOrderOutputs", - args: [orderId, outputs, order.fillDeadline, addressToBytes32(account())] + args: [orderId, outputs, order.fillDeadline, solverBytes32 ?? addressToBytes32(account())] }); const fillReceipt = await getClient(outputChain.id).waitForTransactionReceipt({ hash: transactionHash @@ -312,7 +314,7 @@ export class Solver { const { order, inputSettler } = orderContainer; const intent = containerToIntent(orderContainer); if (intent instanceof StandardSolanaIntent) - throw new Error("Finalise is not supported for Solana input intents."); + throw new Error("Use finaliseSolanaEscrow() for Solana input intents."); if (fillTransactionHashes.length !== order.outputs.length) { throw new Error( `Fill transaction hash count (${fillTransactionHashes.length}) does not match output count (${order.outputs.length}).` diff --git a/src/lib/screens/FillIntent.svelte b/src/lib/screens/FillIntent.svelte index 597bc3a..9699625 100644 --- a/src/lib/screens/FillIntent.svelte +++ b/src/lib/screens/FillIntent.svelte @@ -1,8 +1,16 @@ @@ -123,6 +138,21 @@ description="Fill each chain once and continue to the right. If you refreshed the page provide your fill tx hash in the input box." >
+ {#if isSolanaToEvm} + +
+ + +
+
+ {/if} {#each sortOutputsByChain(orderContainer) as chainIdAndOutputs} @@ -138,6 +168,14 @@ > Fill + {:else if isSolanaToEvm && !isValidSolanaAddress(solanaSolverAddress)} + {:else} v == BYTES32_ZERO) ? "default" : "muted"} @@ -148,11 +186,14 @@ store.walletClient, { orderContainer, - outputs: chainIdAndOutputs[1] + outputs: chainIdAndOutputs[1], + solverBytes32: + isSolanaToEvm && isValidSolanaAddress(solanaSolverAddress) + ? solanaAddressToBytes32(solanaSolverAddress) + : undefined }, { preHook, - postHook: postHookScroll, account } ) diff --git a/src/lib/screens/Finalise.svelte b/src/lib/screens/Finalise.svelte index 16fbabe..ca22ecf 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -1,13 +1,17 @@