From e2b9e1848b9b10e8e8a5024e60bd23ddac47d2ca Mon Sep 17 00:00:00 2001 From: Asem- Abdelhady Date: Fri, 13 Mar 2026 11:38:39 +0100 Subject: [PATCH 1/4] feat: evem to solana --- src/lib/abi/polymeroracle.ts | 52 ++++ src/lib/config.ts | 22 +- src/lib/libraries/flowProgress.ts | 53 +++- src/lib/libraries/solanaFillLib.ts | 359 ++++++++++++++++++++++++++ src/lib/libraries/solver.ts | 168 +++++++++--- src/lib/screens/FillIntent.svelte | 120 +++++++-- src/lib/screens/Finalise.svelte | 10 +- src/lib/screens/ReceiveMessage.svelte | 48 ++-- src/lib/state.svelte.ts | 12 +- src/routes/polymer/+server.ts | 41 ++- tests/fixtures/orderFixtures.ts | 2 +- tests/unit/intentList.test.ts | 2 +- 12 files changed, 772 insertions(+), 117 deletions(-) create mode 100644 src/lib/libraries/solanaFillLib.ts diff --git a/src/lib/abi/polymeroracle.ts b/src/lib/abi/polymeroracle.ts index 6f29738..5c5d542 100644 --- a/src/lib/abi/polymeroracle.ts +++ b/src/lib/abi/polymeroracle.ts @@ -83,6 +83,32 @@ export const POLYMER_ORACLE_ABI = [ outputs: [], stateMutability: "nonpayable" }, + { + type: "function", + name: "receiveSolanaMessage", + inputs: [ + { + name: "proofs", + type: "bytes[]", + internalType: "bytes[]" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, + { + type: "function", + name: "receiveSolanaMessage", + inputs: [ + { + name: "proof", + type: "bytes", + internalType: "bytes" + } + ], + outputs: [], + stateMutability: "nonpayable" + }, { type: "event", name: "OutputProven", @@ -124,6 +150,11 @@ export const POLYMER_ORACLE_ABI = [ name: "ContextOutOfRange", inputs: [] }, + { + type: "error", + name: "InvalidSolanaMessage", + inputs: [] + }, { type: "error", name: "NotDivisible", @@ -145,6 +176,27 @@ export const POLYMER_ORACLE_ABI = [ name: "NotProven", inputs: [] }, + { + type: "error", + name: "NotSolanaMessage", + inputs: [] + }, + { + type: "error", + name: "SolanaProgramIdMismatch", + inputs: [ + { + name: "returnedProgramId", + type: "bytes32", + internalType: "bytes32" + }, + { + name: "messageProgramId", + type: "bytes32", + internalType: "bytes32" + } + ] + }, { type: "error", name: "WrongEventSignature", diff --git a/src/lib/config.ts b/src/lib/config.ts index adbddce..ec1f370 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -109,18 +109,18 @@ export const WORMHOLE_ORACLE: Partial> = { [base.id]: "0x0000000000000000000000000000000000000000" }; export const POLYMER_ORACLE: Partial> = { - [ethereum.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [arbitrum.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [base.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [megaeth.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [katana.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [polygon.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", - [bsc.id]: "0x0000003E06000007A224AeE90052fA6bb46d43C9", + [ethereum.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [arbitrum.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [base.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [megaeth.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [katana.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [polygon.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [bsc.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", // testnet - [sepolia.id]: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00", - [baseSepolia.id]: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00", - [arbitrumSepolia.id]: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00", - [optimismSepolia.id]: "0x00d5b500ECa100F7cdeDC800eC631Aca00BaAC00", + [sepolia.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [baseSepolia.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [arbitrumSepolia.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", + [optimismSepolia.id]: "0xe68b03ff7277fbe1a9c5d1f9c5ec30a220e3cb36", [solanaDevnet.id]: SOLANA_PDAS.devnet.POLYMER_ORACLE, [solanaMainnet.id]: SOLANA_PDAS.mainnet.POLYMER_ORACLE }; diff --git a/src/lib/libraries/flowProgress.ts b/src/lib/libraries/flowProgress.ts index 4549a70..a645056 100644 --- a/src/lib/libraries/flowProgress.ts +++ b/src/lib/libraries/flowProgress.ts @@ -20,6 +20,8 @@ import { addressToBytes32, bytes32ToAddress } from "@lifi/intent"; import { orderToIntent } from "@lifi/intent"; import { getOrFetchRpc } from "$lib/libraries/rpcCache"; import { deriveAttestationPda } from "$lib/libraries/solanaValidateLib"; +import { isSolanaSubmittedFillRecord } from "$lib/libraries/solanaFillLib"; +import { encodeCommonPayload, encodeFillDescription } from "$lib/libraries/solanaValidateLib"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; import store from "$lib/state.svelte"; @@ -41,12 +43,15 @@ export function getOutputStorageKey(output: MandateOutput) { }); } -function isValidHash(hash: string | undefined): hash is `0x${string}` { - return !!hash && hash.startsWith("0x") && hash.length === 66; +function hasFillReference(hash: string | undefined): hash is string { + return typeof hash === "string" && hash.length > 0; } async function isOutputFilled(orderId: `0x${string}`, output: MandateOutput) { const outputKey = getOutputStorageKey(output); + if (isSolanaChain(output.chainId)) { + return Boolean(store.fillTransactions[outputKey]); + } return getOrFetchRpc( `progress:filled:${orderId}:${outputKey}`, async () => { @@ -69,19 +74,46 @@ async function isOutputValidatedOnChain( inputChain: bigint, orderContainer: OrderContainer, output: MandateOutput, - fillTransactionHash: `0x${string}` + fillTransactionHash: string ) { const outputKey = getOutputStorageKey(output); - const cachedReceipt = store.getTransactionReceipt(output.chainId, fillTransactionHash); + if (isSolanaChain(output.chainId)) { + const record = store.getTransactionReceipt(output.chainId, fillTransactionHash); + if (!isSolanaSubmittedFillRecord(record)) return false; + const outputHash = keccak256( + encodeFillDescription( + record.solverBytes32, + orderId, + record.fillTimestamp, + encodeCommonPayload(output) + ) + ); + return getOrFetchRpc( + `progress:solana-proven-evm:${orderId}:${inputChain.toString()}:${outputKey}:${fillTransactionHash}`, + async () => { + const sourceChainClient = getClient(inputChain); + return sourceChainClient.readContract({ + address: orderContainer.order.inputOracle, + abi: POLYMER_ORACLE_ABI, + functionName: "isProven", + args: [output.chainId, output.oracle, output.settler, outputHash] + }); + }, + { ttlMs: PROGRESS_TTL_MS } + ); + } + + const evmFillTransactionHash = fillTransactionHash as `0x${string}`; + const cachedReceipt = store.getTransactionReceipt(output.chainId, evmFillTransactionHash); const receipt = ( cachedReceipt ? cachedReceipt : await getOrFetchRpc( - `progress:receipt:${output.chainId.toString()}:${fillTransactionHash}`, + `progress:receipt:${output.chainId.toString()}:${evmFillTransactionHash}`, async () => { const outputClient = getClient(output.chainId); return outputClient.getTransactionReceipt({ - hash: fillTransactionHash + hash: evmFillTransactionHash }); }, { ttlMs: PROGRESS_TTL_MS } @@ -89,10 +121,11 @@ async function isOutputValidatedOnChain( ) as { blockHash: `0x${string}`; from: `0x${string}`; + logs: unknown[]; }; if (!cachedReceipt) { store - .saveTransactionReceipt(output.chainId, fillTransactionHash, receipt) + .saveTransactionReceipt(output.chainId, evmFillTransactionHash, receipt) .catch((error) => console.warn("saveTransactionReceipt error", error)); } @@ -104,7 +137,7 @@ async function isOutputValidatedOnChain( const logs = parseEventLogs({ abi: COIN_FILLER_ABI, eventName: "OutputFilled", - logs: (receipt as unknown as { logs: any[] }).logs + logs: receipt.logs as never }); const expectedHash = hashStruct({ types: compactTypes, @@ -238,7 +271,7 @@ async function isInputChainFinalised(chainId: bigint, container: OrderContainer) export async function getOrderProgressChecks( orderContainer: OrderContainer, - fillTransactions: Record + fillTransactions: Record ): Promise { try { const intent = orderToIntent(orderContainer); @@ -257,7 +290,7 @@ export async function getOrderProgressChecks( inputChains.flatMap((inputChain) => outputs.map(async (output) => { const fillHash = fillTransactions[getOutputStorageKey(output)]; - if (!isValidHash(fillHash)) return false; + if (!hasFillReference(fillHash)) return false; return isOutputValidatedOnChain(orderId, inputChain, orderContainer, output, fillHash); }) ) diff --git a/src/lib/libraries/solanaFillLib.ts b/src/lib/libraries/solanaFillLib.ts new file mode 100644 index 0000000..6ac7e28 --- /dev/null +++ b/src/lib/libraries/solanaFillLib.ts @@ -0,0 +1,359 @@ +import { keccak256 } from "viem"; +import polymerIdl from "../abi/polymer_oracle.json"; +import { + BYTES32_ZERO, + SOLANA_INTENTS_PROTOCOL, + SOLANA_OUTPUT_SETTLER_SIMPLE, + SOLANA_POLYMER_ORACLE +} from "../config"; +import { getOutputHash } from "@lifi/intent"; +import { encodeCommonPayload } from "./solanaValidateLib"; +import type { MandateOutput } from "@lifi/intent"; + +const OUTPUT_SETTLER_SIMPLE_IDL = { + address: SOLANA_OUTPUT_SETTLER_SIMPLE, + metadata: { + name: "outputSettlerSimple", + version: "0.0.0", + spec: "0.1.0" + }, + instructions: [ + { + name: "fill", + discriminator: [168, 96, 183, 163, 92, 10, 40, 160], + accounts: [ + { name: "filler", writable: true, signer: true }, + { name: "recipient", writable: true }, + { name: "outputSettlerSimple" }, + { name: "fillerTokenAccount", writable: true }, + { name: "recipientTokenAccount", writable: true }, + { name: "mint" }, + { name: "fillId", writable: true }, + { name: "localAttestation", writable: true }, + { name: "intentsProtocolProgram" }, + { name: "tokenProgram" }, + { name: "associatedTokenProgram" }, + { name: "systemProgram" } + ], + args: [ + { name: "orderId", type: { array: ["u8", 32] } }, + { name: "mandateOutput", type: { defined: { name: "mandateOutput" } } }, + { name: "fillDeadline", type: "u64" }, + { name: "fillerData", type: "bytes" } + ] + }, + { + name: "nativeFill", + discriminator: [49, 10, 255, 151, 120, 148, 73, 30], + accounts: [ + { name: "filler", writable: true, signer: true }, + { name: "recipient", writable: true }, + { name: "outputSettlerSimple" }, + { name: "fillId", writable: true }, + { name: "localAttestation", writable: true }, + { name: "intentsProtocolProgram" }, + { name: "systemProgram" } + ], + args: [ + { name: "orderId", type: { array: ["u8", 32] } }, + { name: "mandateOutput", type: { defined: { name: "mandateOutput" } } }, + { name: "fillDeadline", type: "u64" }, + { name: "fillerData", type: "bytes" } + ] + } + ], + types: [ + { + name: "mandateOutput", + type: { + kind: "struct", + fields: [ + { name: "oracle", type: { array: ["u8", 32] } }, + { name: "settler", type: { array: ["u8", 32] } }, + { name: "chainId", type: { array: ["u8", 32] } }, + { name: "token", type: { array: ["u8", 32] } }, + { name: "amount", type: { array: ["u8", 32] } }, + { name: "recipient", type: { array: ["u8", 32] } }, + { name: "callbackData", type: "bytes" }, + { name: "context", type: "bytes" } + ] + } + } + ] +} as const; + +export const SOLANA_POLYMER_SOURCE_CHAIN_ID = 2; + +export type SolanaSubmittedFillRecord = { + kind: "solanaOutputSubmittedFill"; + fillSignature: string; + submitSignature: string; + fillTimestamp: number; + solverBytes32: `0x${string}`; + localAttestation: string; + submitSlot: number; + submitLogIndex: number; + orderId: `0x${string}`; +}; + +export function isSolanaSubmittedFillRecord(value: unknown): value is SolanaSubmittedFillRecord { + if (!value || typeof value !== "object") return false; + return (value as { kind?: string }).kind === "solanaOutputSubmittedFill"; +} + +function hexToBytes32(hex: `0x${string}`): number[] { + return Array.from(Buffer.from(hex.slice(2), "hex")); +} + +function bigintToBeBytes32(n: bigint): number[] { + return Array.from(Buffer.from(n.toString(16).padStart(64, "0"), "hex")); +} + +function encodeFillDescriptionWithoutTimestamp( + solverBytes32: `0x${string}`, + orderId: `0x${string}`, + commonPayload: Buffer +): Buffer { + return Buffer.concat([ + Buffer.from(solverBytes32.slice(2), "hex"), + Buffer.from(orderId.slice(2), "hex"), + commonPayload + ]); +} + +function encodeFillDescription( + solverBytes32: `0x${string}`, + orderId: `0x${string}`, + timestamp: number, + commonPayload: Buffer +): Buffer { + const ts = Buffer.alloc(4); + ts.writeUInt32BE(timestamp >>> 0, 0); + return Buffer.concat([ + Buffer.from(solverBytes32.slice(2), "hex"), + Buffer.from(orderId.slice(2), "hex"), + ts, + commonPayload + ]); +} + +function findProveLogIndex(logMessages: string[]): number { + return logMessages.findIndex((entry) => entry.includes("Program log: Prove: program:")); +} + +async function computeGlobalLogIndex( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connection: any, + slot: number, + signature: string, + transactionLogIndex: number +): Promise { + const block = await connection.getBlock(slot, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + transactionDetails: "full", + rewards: false + }); + if (!block?.transactions) return transactionLogIndex; + + let logOffset = 0; + for (const tx of block.transactions) { + const signatures = tx.transaction.signatures as string[]; + const logMessages = tx.meta?.logMessages ?? []; + if (signatures.includes(signature)) { + return logOffset + transactionLogIndex; + } + logOffset += logMessages.length; + } + + return transactionLogIndex; +} + +export async function fillAndSubmitSolanaOutput(params: { + orderId: `0x${string}`; + output: MandateOutput; + fillDeadline: number; + solverBytes32: `0x${string}`; + solanaPublicKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + walletAdapter: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + connection: any; +}): Promise { + const { BN, AnchorProvider, Program } = await import("@coral-xyz/anchor"); + const { PublicKey, SystemProgram } = await import("@solana/web3.js"); + const { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } = + await import("@solana/spl-token"); + + const { + orderId, + output, + fillDeadline, + solverBytes32, + solanaPublicKey, + walletAdapter, + connection + } = params; + const filler = new PublicKey(solanaPublicKey); + const outputSettlerProgramId = new PublicKey(SOLANA_OUTPUT_SETTLER_SIMPLE); + const intentsProtocolProgramId = new PublicKey(SOLANA_INTENTS_PROTOCOL); + const polymerProgramId = new PublicKey(SOLANA_POLYMER_ORACLE); + + const anchorWallet = { + publicKey: filler, + signTransaction: (tx: unknown) => walletAdapter.signTransaction(tx), + signAllTransactions: (txs: unknown[]) => walletAdapter.signAllTransactions(txs) + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const provider = new AnchorProvider(connection, anchorWallet as any, { commitment: "confirmed" }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const outputSettlerProgram = new Program(OUTPUT_SETTLER_SIMPLE_IDL as any, provider); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const polymerProgram = new Program(polymerIdl as any, provider); + + const [outputSettlerPda] = PublicKey.findProgramAddressSync( + [Buffer.from("output_settler_simple")], + outputSettlerProgramId + ); + const [polymerOraclePda] = PublicKey.findProgramAddressSync( + [Buffer.from("polymer")], + polymerProgramId + ); + const outputHash = Buffer.from(getOutputHash(output).slice(2), "hex"); + const [fillId] = PublicKey.findProgramAddressSync( + [Buffer.from(orderId.slice(2), "hex"), outputHash], + outputSettlerProgramId + ); + + const commonPayload = encodeCommonPayload(output); + const dataHash = Buffer.from( + keccak256(encodeFillDescriptionWithoutTimestamp(solverBytes32, orderId, commonPayload)).slice( + 2 + ), + "hex" + ); + const [localAttestation] = PublicKey.findProgramAddressSync( + [ + Buffer.from("local_attestation"), + outputSettlerPda.toBuffer(), + Buffer.from(output.oracle.slice(2), "hex"), + dataHash + ], + intentsProtocolProgramId + ); + + const outputArg = { + oracle: hexToBytes32(output.oracle), + settler: hexToBytes32(output.settler), + chainId: bigintToBeBytes32(output.chainId), + token: hexToBytes32(output.token), + amount: bigintToBeBytes32(output.amount), + recipient: hexToBytes32(output.recipient), + callbackData: + output.callbackData === "0x" + ? Buffer.alloc(0) + : Buffer.from(output.callbackData.slice(2), "hex"), + context: output.context === "0x" ? Buffer.alloc(0) : Buffer.from(output.context.slice(2), "hex") + }; + + let fillSignature: string; + if (output.token === BYTES32_ZERO) { + const recipient = new PublicKey(Buffer.from(output.recipient.slice(2), "hex")); + fillSignature = await outputSettlerProgram.methods + .nativeFill( + Array.from(Buffer.from(orderId.slice(2), "hex")), + outputArg as never, + new BN(fillDeadline), + Buffer.from(solverBytes32.slice(2), "hex") + ) + .accounts({ + filler, + recipient, + outputSettlerSimple: outputSettlerPda, + fillId, + localAttestation, + intentsProtocolProgram: intentsProtocolProgramId, + systemProgram: SystemProgram.programId + }) + .rpc({ commitment: "confirmed" }); + } else { + const recipient = new PublicKey(Buffer.from(output.recipient.slice(2), "hex")); + const mint = new PublicKey(Buffer.from(output.token.slice(2), "hex")); + const fillerTokenAccount = getAssociatedTokenAddressSync(mint, filler, false); + const recipientTokenAccount = getAssociatedTokenAddressSync(mint, recipient, false); + fillSignature = await outputSettlerProgram.methods + .fill( + Array.from(Buffer.from(orderId.slice(2), "hex")), + outputArg as never, + new BN(fillDeadline), + Buffer.from(solverBytes32.slice(2), "hex") + ) + .accounts({ + filler, + recipient, + outputSettlerSimple: outputSettlerPda, + fillerTokenAccount, + recipientTokenAccount, + mint, + fillId, + localAttestation, + intentsProtocolProgram: intentsProtocolProgramId, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId + }) + .rpc({ commitment: "confirmed" }); + } + + const localAttestationInfo = await connection.getAccountInfo(localAttestation, "confirmed"); + if (!localAttestationInfo || localAttestationInfo.data.length < 13) { + throw new Error("Could not read Solana local attestation after fill"); + } + const fillTimestamp = localAttestationInfo.data.readUInt32LE(8); + const fillDescription = encodeFillDescription( + solverBytes32, + orderId, + fillTimestamp, + commonPayload + ); + + const submitSignature = await polymerProgram.methods + .submit(outputSettlerPda, [fillDescription] as never) + .accounts({ + submitter: filler, + oraclePolymer: polymerOraclePda, + intentsProtocolProgram: intentsProtocolProgramId + }) + .remainingAccounts([{ pubkey: localAttestation, isWritable: false, isSigner: false }]) + .rpc({ commitment: "confirmed" }); + + const submitTx = await connection.getTransaction(submitSignature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0 + }); + if (!submitTx?.meta?.logMessages || submitTx.slot === undefined) { + throw new Error("Could not load Solana polymer submit transaction logs"); + } + const submitLogIndex = findProveLogIndex(submitTx.meta.logMessages); + if (submitLogIndex === -1) { + throw new Error("Could not find Polymer prove log in Solana submit transaction"); + } + const submitGlobalLogIndex = await computeGlobalLogIndex( + connection, + submitTx.slot, + submitSignature, + submitLogIndex + ); + + return { + kind: "solanaOutputSubmittedFill", + fillSignature, + submitSignature, + fillTimestamp, + solverBytes32, + localAttestation: localAttestation.toBase58(), + submitSlot: submitTx.slot, + submitLogIndex: submitGlobalLogIndex, + orderId + }; +} diff --git a/src/lib/libraries/solver.ts b/src/lib/libraries/solver.ts index 35e8300..a18e4c6 100644 --- a/src/lib/libraries/solver.ts +++ b/src/lib/libraries/solver.ts @@ -1,4 +1,13 @@ -import { BYTES32_ZERO, COIN_FILLER, getChain, getClient, getOracle, type WC } from "$lib/config"; +import { + BYTES32_ZERO, + COIN_FILLER, + getChain, + getClient, + getOracle, + chainMap, + SOLANA_POLYMER_ORACLE, + type WC +} from "$lib/config"; import { hashStruct, maxUint256, parseEventLogs } from "viem"; import type { MandateOutput, OrderContainer } from "@lifi/intent"; import { addressToBytes32, bytes32ToAddress } from "@lifi/intent"; @@ -10,6 +19,7 @@ import { orderToIntent, StandardOrderIntent, MultichainOrderIntent } from "@lifi import { compactTypes } from "@lifi/intent"; import store from "$lib/state.svelte"; import { finaliseIntent } from "./intentExecution"; +import { SOLANA_POLYMER_SOURCE_CHAIN_ID, isSolanaSubmittedFillRecord } from "./solanaFillLib"; /** * @notice Class for solving intents. Functions called by solvers. @@ -22,11 +32,7 @@ export class Solver { return new Promise((resolve) => setTimeout(resolve, ms)); } - private static async persistReceipt( - chainId: number | bigint, - txHash: `0x${string}`, - receipt: unknown - ) { + private static async persistReceipt(chainId: number | bigint, txHash: string, receipt: unknown) { try { await store.saveTransactionReceipt(chainId, txHash, receipt); } catch (error) { @@ -48,6 +54,14 @@ export class Solver { return receipt; } + private static getSolanaFillRecord(output: MandateOutput, txRef: string) { + const record = store.getTransactionReceipt(output.chainId, txRef); + if (!isSolanaSubmittedFillRecord(record)) { + throw new Error(`Missing Solana fill metadata for output ${txRef}`); + } + return record; + } + static fill( walletClient: WC, args: { @@ -170,11 +184,66 @@ export class Solver { if (existingValidation) return existingValidation; const validationPromise = (async () => { - if ( - !fillTransactionHash || - !fillTransactionHash.startsWith("0x") || - fillTransactionHash.length !== 66 - ) { + if (!fillTransactionHash) { + throw new Error(`Invalid fill transaction reference: ${fillTransactionHash}`); + } + + if (output.chainId === BigInt(chainMap.solanaDevnet.id)) { + const record = Solver.getSolanaFillRecord(output, fillTransactionHash); + let proof: string | undefined; + const polymerKey = `${SOLANA_POLYMER_SOURCE_CHAIN_ID}:${record.submitSignature}:${SOLANA_POLYMER_ORACLE}`; + let polymerIndex: number | undefined = Solver.polymerRequestIndexByLog.get(polymerKey); + for (const waitMs of [1000, 2000, 4000, 8000, 16000, 32000]) { + const response = await axios.post( + `/polymer`, + { + srcChainId: SOLANA_POLYMER_SOURCE_CHAIN_ID, + txSignature: record.submitSignature, + programID: SOLANA_POLYMER_ORACLE, + polymerIndex, + mainnet: mainnet + }, + { timeout: 15_000 } + ); + const dat = response.data as { + proof: undefined | string; + polymerIndex: number; + }; + polymerIndex = dat.polymerIndex; + if (polymerIndex !== undefined) { + Solver.polymerRequestIndexByLog.set(polymerKey, polymerIndex); + } + if (dat.proof) { + proof = dat.proof; + break; + } + await Solver.sleep(waitMs); + } + if (!proof) { + throw new Error( + `Polymer proof unavailable for Solana output. Request used srcChainId=${SOLANA_POLYMER_SOURCE_CHAIN_ID}, txSignature=${record.submitSignature}, programID=${SOLANA_POLYMER_ORACLE}. Try again after the submit transaction is indexed.` + ); + } + if (preHook) await preHook(Number(sourceChainId)); + const transactionHash = await walletClient.writeContract({ + chain: getChain(sourceChainId), + account: account(), + address: order.inputOracle, + abi: POLYMER_ORACLE_ABI, + functionName: "receiveSolanaMessage", + args: [`0x${proof.replace("0x", "")}`] + }); + const result = await getClient(sourceChainId).waitForTransactionReceipt({ + hash: transactionHash, + timeout: 120_000, + pollingInterval: 2_000 + }); + await Solver.persistReceipt(sourceChainId, transactionHash, result); + if (postHook) await postHook(); + return result; + } + + if (!fillTransactionHash.startsWith("0x") || fillTransactionHash.length !== 66) { throw new Error(`Invalid fill transaction hash: ${fillTransactionHash}`); } @@ -321,26 +390,6 @@ export class Solver { `Fill transaction hash count (${fillTransactionHashes.length}) does not match output count (${order.outputs.length}).` ); } - for (let i = 0; i < fillTransactionHashes.length; i++) { - const hash = fillTransactionHashes[i]; - if (!hash || !hash.startsWith("0x") || hash.length !== 66) { - throw new Error(`Invalid fill tx hash at index ${i}: ${hash}`); - } - } - const transactionReceipts = await Promise.all( - fillTransactionHashes.map((fth, i) => - Solver.getReceiptCachedOrRpc(order.outputs[i].chainId, fth as `0x${string}`) - ) - ); - const blocks = await Promise.all( - transactionReceipts.map((r, i) => { - return getClient(order.outputs[i].chainId).getBlock({ - blockHash: r.blockHash - }); - }) - ); - const fillTimestamps = blocks.map((b) => b.timestamp); - if (preHook) await preHook(Number(sourceChainId)); const expectedChainId = Number(sourceChainId); const connectedChainId = await walletClient.getChainId(); @@ -350,12 +399,57 @@ export class Solver { ); } - const solveParams = fillTimestamps.map((fillTimestamp) => { - return { - timestamp: Number(fillTimestamp), - solver: addressToBytes32(account()) - }; - }); + const solveParams = await Promise.all( + order.outputs.map(async (output, i) => { + const txRef = fillTransactionHashes[i]; + if (!txRef) { + throw new Error(`Missing fill transaction reference at index ${i}`); + } + if (output.chainId === BigInt(chainMap.solanaDevnet.id)) { + const record = Solver.getSolanaFillRecord(output, txRef); + return { + timestamp: record.fillTimestamp, + solver: record.solverBytes32 + }; + } + if (!txRef.startsWith("0x") || txRef.length !== 66) { + throw new Error(`Invalid fill tx hash at index ${i}: ${txRef}`); + } + const transactionReceipt = await Solver.getReceiptCachedOrRpc( + output.chainId, + txRef as `0x${string}` + ); + const logs = parseEventLogs({ + abi: COIN_FILLER_ABI, + eventName: "OutputFilled", + logs: transactionReceipt.logs + }); + const expectedOutputHash = 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 === expectedOutputHash; + }); + if (!matchingLog) { + throw new Error(`Could not find matching OutputFilled log for output ${i}`); + } + const fillTimestamp = + typeof matchingLog.args.timestamp === "number" + ? matchingLog.args.timestamp + : Number(matchingLog.args.timestamp); + return { + timestamp: fillTimestamp, + solver: matchingLog.args.solver as `0x${string}` + }; + }) + ); const transactionHash = await finaliseIntent({ intent, diff --git a/src/lib/screens/FillIntent.svelte b/src/lib/screens/FillIntent.svelte index bd5eccb..172b6e4 100644 --- a/src/lib/screens/FillIntent.svelte +++ b/src/lib/screens/FillIntent.svelte @@ -1,12 +1,22 @@ + {:else if hasSolanaOutput} + +
+ + +
+
{/if} {#each sortOutputsByChain(orderContainer) as chainIdAndOutputs} @@ -153,6 +223,7 @@ {@const chainStatuses = chainIdAndOutputs[1].map( (output) => fillStatuses[outputKey(output)] )} + {@const isSolanaOutputChain = chainIdAndOutputs[0] === BigInt(chainMap.solanaDevnet.id)} {#if chainStatuses.some((status) => status === undefined)} +<<<<<<< HEAD {:else if isSolanaToEvm && !isValidSolanaAddress(solanaSolverAddress)} + {:else if isSolanaOutputChain && !solanaWallet.connected} + {:else} v == BYTES32_ZERO) ? "default" : "muted"} buttonFunction={chainStatuses.every((v) => v == BYTES32_ZERO) - ? fillWrapper( - chainIdAndOutputs[1], - Solver.fill( - store.walletClient, - { - orderContainer, - outputs: chainIdAndOutputs[1], - solverBytes32: - isSolanaToEvm && isValidSolanaAddress(solanaSolverAddress) - ? solanaAddressToBytes32(solanaSolverAddress) - : undefined - }, - { - preHook, - account - } + ? isSolanaOutputChain + ? solanaFillWrapper(chainIdAndOutputs[1]) + : fillWrapper( + chainIdAndOutputs[1], + Solver.fill( + store.walletClient, + { + orderContainer, + outputs: chainIdAndOutputs[1], + solverBytes32: + isSolanaToEvm && isValidSolanaAddress(solanaSolverAddress) + ? solanaAddressToBytes32(solanaSolverAddress) + : undefined + }, + { + preHook, + account + } + ) ) - ) : async () => {}} > {#snippet name()} diff --git a/src/lib/screens/Finalise.svelte b/src/lib/screens/Finalise.svelte index f266cf5..9e3dc70 100644 --- a/src/lib/screens/Finalise.svelte +++ b/src/lib/screens/Finalise.svelte @@ -85,8 +85,10 @@ const fillTransactionHashesFor = (container: OrderContainer) => container.order.outputs.map((output) => store.fillTransactions[outputKey(output)]); - const isValidFillTxHash = (hash: unknown): hash is `0x${string}` => - typeof hash === "string" && hash.startsWith("0x") && hash.length === 66; + const hasClaimFillReference = (inputChain: bigint, hash: unknown): hash is string => + isSolanaChain(inputChain) + ? typeof hash === "string" && hash.startsWith("0x") && hash.length === 66 + : typeof hash === "string" && hash.length > 0; // Order status enum const OrderStatus_None = 0; @@ -322,7 +324,9 @@ {:else} {@const fillTransactionHashes = fillTransactionHashesFor(orderContainer)} - {@const canClaim = fillTransactionHashes.every((hash) => isValidFillTxHash(hash))} + {@const canClaim = fillTransactionHashes.every((hash) => + hasClaimFillReference(inputChain, hash) + )} {#if !canClaim} -<<<<<<< HEAD {:else if isSolanaToEvm && !isValidSolanaAddress(solanaSolverAddress)}