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/config.ts b/src/lib/config.ts index 9058a0c..19f78a4 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -93,7 +93,8 @@ export const SOLANA_PROGRAMS: { INTENTS_PROTOCOL: "H1dVz9YXVys8c4tAihD14M5jnrUQi1MFsA65YQ92oCCz", OUTPUT_SETTLER_SIMPLE: "58CsNaufL383JL7J1jafGW4eWgeQFX5vSZssjsk4WKXd", INPUT_SETTLER_ESCROW: "5QngyaYhNscSebqV4DwYQhk333p5CMP8A9yyLX3pPyXC", - POLYMER_ORACLE: "C2rAFLS6xQ78t18rK5s9madY9fztbhTaHwShgYtzonk7" + POLYMER_ORACLE: "C2rAFLS6xQ78t18rK5s9madY9fztbhTaHwShgYtzonk7", + POLYMER_PROVER: "CdvSq48QUukYuMczgZAVNZrwcHNshBdtqrjW26sQiGPs" }, mainnet: { // Mainnet program IDs are not yet deployed. Remove solanaMainnet from chainList(true) @@ -101,7 +102,8 @@ export const SOLANA_PROGRAMS: { INTENTS_PROTOCOL: null, OUTPUT_SETTLER_SIMPLE: null, INPUT_SETTLER_ESCROW: null, - POLYMER_ORACLE: null + POLYMER_ORACLE: null, + POLYMER_PROVER: null } }; @@ -131,11 +133,12 @@ export const SOLANA_PDAS = { } as const; // Flat exports for use throughout the codebase -export const SOLANA_INTENTS_PROTOCOL = SOLANA_PROGRAMS.devnet.INTENTS_PROTOCOL; -export const SOLANA_OUTPUT_SETTLER_SIMPLE = SOLANA_PROGRAMS.devnet.OUTPUT_SETTLER_SIMPLE; -export const SOLANA_INPUT_SETTLER_ESCROW = SOLANA_PROGRAMS.devnet.INPUT_SETTLER_ESCROW; -export const SOLANA_POLYMER_ORACLE = SOLANA_PROGRAMS.devnet.POLYMER_ORACLE; -export const SOLANA_OUTPUT_SETTLER_PDA = SOLANA_PDAS.devnet.OUTPUT_SETTLER; +export const SOLANA_DEVNET_INTENTS_PROTOCOL = SOLANA_PROGRAMS.devnet.INTENTS_PROTOCOL; +export const SOLANA_DEVNET_OUTPUT_SETTLER_SIMPLE = SOLANA_PROGRAMS.devnet.OUTPUT_SETTLER_SIMPLE; +export const SOLANA_DEVNET_INPUT_SETTLER_ESCROW = SOLANA_PROGRAMS.devnet.INPUT_SETTLER_ESCROW; +export const SOLANA_DEVNET_POLYMER_ORACLE = SOLANA_PROGRAMS.devnet.POLYMER_ORACLE; +export const SOLANA_DEVNET_POLYMER_PROVER_PROGRAM = SOLANA_PROGRAMS.devnet.POLYMER_PROVER; +export const SOLANA_DEVNET_OUTPUT_SETTLER_PDA = SOLANA_PDAS.devnet.OUTPUT_SETTLER; export const WORMHOLE_ORACLE: Partial> = { [ethereum.id]: "0x0000000000000000000000000000000000000000", diff --git a/src/lib/libraries/evmFillLib.ts b/src/lib/libraries/evmFillLib.ts new file mode 100644 index 0000000..dd79f5b --- /dev/null +++ b/src/lib/libraries/evmFillLib.ts @@ -0,0 +1,44 @@ +import { hashStruct, parseEventLogs } from "viem"; +import type { TransactionReceipt } from "viem"; +import { COIN_FILLER_ABI } from "$lib/abi/outputsettler"; +import { compactTypes } from "@lifi/intent"; +import type { MandateOutput } from "@lifi/intent"; + +/** + * Find the OutputFilled log in a fill receipt that matches the given MandateOutput by structural hash. + * Returns the extracted solver, timestamp, proof output, emitting contract, and log index — or null if not found. + */ +export function findOutputFilledLog( + receipt: TransactionReceipt, + output: MandateOutput +): { + solverBytes32: `0x${string}`; + fillTimestamp: number; + proofOutput: MandateOutput; + emittingContract: `0x${string}`; + logIndex: number; +} | null { + const logs = parseEventLogs({ + abi: COIN_FILLER_ABI, + eventName: "OutputFilled", + logs: receipt.logs + }); + const expectedHash = hashStruct({ + types: compactTypes, + primaryType: "MandateOutput", + data: output + }); + const match = logs.find( + (log) => + hashStruct({ types: compactTypes, primaryType: "MandateOutput", data: log.args.output }) === + expectedHash + ); + if (!match) return null; + return { + solverBytes32: match.args.solver as `0x${string}`, + fillTimestamp: Number(match.args.timestamp), + proofOutput: match.args.output as MandateOutput, + emittingContract: match.address as `0x${string}`, + logIndex: match.logIndex + }; +} diff --git a/src/lib/libraries/flowProgress.ts b/src/lib/libraries/flowProgress.ts index 9c89091..c2347e7 100644 --- a/src/lib/libraries/flowProgress.ts +++ b/src/lib/libraries/flowProgress.ts @@ -5,19 +5,25 @@ 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 type { TransactionReceipt } 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 { findOutputFilledLog } from "$lib/libraries/evmFillLib"; +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; @@ -83,16 +89,39 @@ async function isOutputValidatedOnChain( }, { ttlMs: PROGRESS_TTL_MS } ) - ) as { - blockHash: `0x${string}`; - from: `0x${string}`; - }; + ) as TransactionReceipt; if (!cachedReceipt) { store .saveTransactionReceipt(output.chainId, fillTransactionHash, receipt) .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 match = findOutputFilledLog(receipt, output); + if (!match) throw new Error("OutputFilled event not found in fill receipt"); + const attestationPda = await deriveAttestationPda({ + evmChainId: output.chainId, + output, + proofOutput: match.proofOutput, + orderId, + fillTimestamp: match.fillTimestamp, + solverBytes32: match.solverBytes32, + emittingContract: match.emittingContract + }); + 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 +156,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 @@ -184,50 +229,39 @@ export async function getOrderProgressChecks( orderContainer: OrderContainer, fillTransactions: Record ): Promise { - try { - const intent = containerToIntent(orderContainer); - const orderId = intent.orderId(); - const inputChains = intent.inputChains(); - const outputs = orderContainer.order.outputs; - - const filledStates = await Promise.all( - outputs.map((output) => isOutputFilled(orderId, output)) + const intent = containerToIntent(orderContainer); + const orderId = intent.orderId(); + const inputChains = intent.inputChains(); + const outputs = orderContainer.order.outputs; + + const filledStates = await Promise.all(outputs.map((output) => isOutputFilled(orderId, output))); + const allFilled = outputs.length > 0 && filledStates.every(Boolean); + + let allValidated = false; + if (allFilled && inputChains.length > 0) { + const validatedPairs = await Promise.all( + inputChains.flatMap((inputChain) => + outputs.map(async (output) => { + const fillHash = fillTransactions[getOutputStorageKey(output)]; + if (!isValidHash(fillHash)) return false; + return isOutputValidatedOnChain(orderId, inputChain, orderContainer, output, fillHash); + }) + ) ); - const allFilled = outputs.length > 0 && filledStates.every(Boolean); - - let allValidated = false; - if (allFilled && inputChains.length > 0) { - const validatedPairs = await Promise.all( - inputChains.flatMap((inputChain) => - outputs.map(async (output) => { - const fillHash = fillTransactions[getOutputStorageKey(output)]; - if (!isValidHash(fillHash)) return false; - return isOutputValidatedOnChain(orderId, inputChain, orderContainer, output, fillHash); - }) - ) - ); - allValidated = validatedPairs.length > 0 && validatedPairs.every(Boolean); - } - - let allFinalised = false; - if (allValidated && inputChains.length > 0) { - const finalisedStates = await Promise.all( - inputChains.map((chainId) => isInputChainFinalised(chainId, orderContainer)) - ); - allFinalised = finalisedStates.every(Boolean); - } - - return { - allFilled, - allValidated, - allFinalised - }; - } catch (error) { - console.warn("progress checks failed", error); - return { - allFilled: false, - allValidated: false, - allFinalised: false - }; + allValidated = validatedPairs.length > 0 && validatedPairs.every(Boolean); } + + let allFinalised = false; + if (allValidated && inputChains.length > 0) { + const finalisedStates = await Promise.all( + inputChains.map((chainId) => isInputChainFinalised(chainId, orderContainer)) + ); + allFinalised = finalisedStates.every(Boolean); + } + + return { + allFilled, + allValidated, + allFinalised + }; } diff --git a/src/lib/libraries/intentFactory.ts b/src/lib/libraries/intentFactory.ts index 092ddee..cfaaee0 100644 --- a/src/lib/libraries/intentFactory.ts +++ b/src/lib/libraries/intentFactory.ts @@ -3,7 +3,7 @@ import { getClient, getSolanaConnection, isSolanaChain, - SOLANA_INPUT_SETTLER_ESCROW, + SOLANA_DEVNET_INPUT_SETTLER_ESCROW, INPUT_SETTLER_COMPACT_LIFI, INPUT_SETTLER_ESCROW_LIFI, MULTICHAIN_INPUT_SETTLER_ESCROW, @@ -252,7 +252,7 @@ export class IntentFactory { this.saveOrder({ order: solanaOrder, - inputSettler: solanaAddressToBytes32(SOLANA_INPUT_SETTLER_ESCROW) + inputSettler: solanaAddressToBytes32(SOLANA_DEVNET_INPUT_SETTLER_ESCROW) }); } else { if (this.preHook) await this.preHook(inputChain); diff --git a/src/lib/libraries/solanaEscrowLib.ts b/src/lib/libraries/solanaEscrowLib.ts index 96cd567..095acb7 100644 --- a/src/lib/libraries/solanaEscrowLib.ts +++ b/src/lib/libraries/solanaEscrowLib.ts @@ -1,6 +1,6 @@ import { keccak256 } from "viem"; import idl from "../abi/input_settler_escrow.json"; -import { SOLANA_INPUT_SETTLER_ESCROW, SOLANA_POLYMER_ORACLE } from "../config"; +import { SOLANA_DEVNET_INPUT_SETTLER_ESCROW, SOLANA_DEVNET_POLYMER_ORACLE } from "../config"; import type { MandateOutput, StandardSolana } from "@lifi/intent"; import type { SignerWalletAdapter } from "@solana/wallet-adapter-base"; import type { Connection, Transaction, VersionedTransaction } from "@solana/web3.js"; @@ -46,8 +46,8 @@ export async function openSolanaEscrow(params: { await import("@solana/spl-token"); const userPubkey = new PublicKey(solanaPublicKey); - const inputSettlerProgramId = new PublicKey(SOLANA_INPUT_SETTLER_ESCROW); - const polymerProgramId = new PublicKey(SOLANA_POLYMER_ORACLE); + const inputSettlerProgramId = new PublicKey(SOLANA_DEVNET_INPUT_SETTLER_ESCROW); + const polymerProgramId = new PublicKey(SOLANA_DEVNET_POLYMER_ORACLE); // Wrap the wallet adapter as an Anchor-compatible wallet. const anchorWallet = { @@ -70,13 +70,13 @@ export async function openSolanaEscrow(params: { // Program converts the IDL to camelCase internally; its coder uses camelCase field names. const program = new Program(typedIdl, provider); - // Derive polymer oracle PDA (seed: "polymer", program: SOLANA_POLYMER_ORACLE) + // Derive polymer oracle PDA (seed: "polymer", program: SOLANA_DEVNET_POLYMER_ORACLE) const [polymerOraclePda] = PublicKey.findProgramAddressSync( [Buffer.from("polymer")], polymerProgramId ); - // Derive input settler escrow PDA (seed: "input_settler_escrow", program: SOLANA_INPUT_SETTLER_ESCROW) + // Derive input settler escrow PDA (seed: "input_settler_escrow", program: SOLANA_DEVNET_INPUT_SETTLER_ESCROW) const [inputSettlerEscrowPda] = PublicKey.findProgramAddressSync( [Buffer.from("input_settler_escrow")], inputSettlerProgramId @@ -85,7 +85,11 @@ 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 rawToken = order.inputs[0][0]; + if (rawToken === undefined || rawToken === null) { + throw new Error("StandardSolana order is missing inputs[0][0] (token)"); + } + const tokenIdHex = BigInt(rawToken).toString(16).padStart(64, "0"); const inputMint = new PublicKey(Buffer.from(tokenIdHex, "hex")); const inputAmount = new BN(order.inputs[0][1].toString()); @@ -123,7 +127,7 @@ export async function openSolanaEscrow(params: { const orderIdHex = keccak256(encoded); const orderId = Buffer.from(orderIdHex.slice(2), "hex"); - // Derive orderContext PDA (seeds: ["order_context", orderId], program: SOLANA_INPUT_SETTLER_ESCROW) + // Derive orderContext PDA (seeds: ["order_context", orderId], program: SOLANA_DEVNET_INPUT_SETTLER_ESCROW) const [orderContext] = PublicKey.findProgramAddressSync( [Buffer.from("order_context"), orderId], inputSettlerProgramId diff --git a/src/lib/libraries/solanaFinaliseLib.ts b/src/lib/libraries/solanaFinaliseLib.ts new file mode 100644 index 0000000..46c6e3f --- /dev/null +++ b/src/lib/libraries/solanaFinaliseLib.ts @@ -0,0 +1,194 @@ +import { keccak256 } from "viem"; +import idl from "../abi/input_settler_escrow.json"; +import { + SOLANA_DEVNET_INPUT_SETTLER_ESCROW, + SOLANA_DEVNET_INTENTS_PROTOCOL, + SOLANA_DEVNET_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_DEVNET_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_DEVNET_INPUT_SETTLER_ESCROW); + const polymerOracleProgram = new PublicKey(SOLANA_DEVNET_POLYMER_ORACLE); + const intentsProtocolId = new PublicKey(SOLANA_DEVNET_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 [rawToken, inputAmount] = order.inputs[0]; + if (rawToken === undefined || rawToken === null) { + throw new Error("StandardSolana order is missing inputs[0][0] (token)"); + } + const tokenIdHex = BigInt(rawToken).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..a13e11b --- /dev/null +++ b/src/lib/libraries/solanaValidateLib.ts @@ -0,0 +1,278 @@ +import axios from "axios"; +import { keccak256 } from "viem"; +import idl from "../abi/polymer.json"; +import { + SOLANA_DEVNET_INTENTS_PROTOCOL, + SOLANA_DEVNET_POLYMER_ORACLE, + SOLANA_DEVNET_POLYMER_PROVER_PROGRAM +} from "../config"; +import type { MandateOutput } from "@lifi/intent"; +import type { SignerWalletAdapter } from "@solana/wallet-adapter-base"; +import type { Connection } from "@solana/web3.js"; + +/** 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_DEVNET_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_DEVNET_POLYMER_ORACLE); + const intentsProtocol = new PublicKey(SOLANA_DEVNET_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); + + // Seed order is fixed by the on-chain intents_protocol program — any change silently produces + // the wrong address. Order: [b"attestation", polymer_oracle_pda, chain_id_le16, + // source (oracle bytes32), application (settler/emitting-contract bytes32), payload_hash] + 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_DEVNET_POLYMER_ORACLE); + const polymerProverProgramId = new PublicKey(SOLANA_DEVNET_POLYMER_PROVER_PROGRAM); + const intentsProtocolId = new PublicKey(SOLANA_DEVNET_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]) { + 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 + ); + + // Same seed order as deriveAttestationPda — must stay in sync with the on-chain program. + 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..7c1a065 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -1,13 +1,18 @@