@@ -575,6 +575,88 @@ For a different integrator (e.g., a custom one not following the LotPot
575575shape), the contract above is the same — only the ABI fragments and event
576576parser change. The widget never knows the difference.
577577
578+ ### TradeStars offramp v2 (allocation-funded)
579+
580+ The TradeStars offramp funds the SELL from a ** per-user-proxy allocation** (a
581+ relayer moves vault USDC into the user's proxy after a Solana burn), not the
582+ user's wallet USDC. Three differences vs the LotPot recipe:
583+
584+ 1 . ** No USDC approve** — the proxy already holds the funds; ` placeCashout ` just
585+ calls ` userStartOfframp(allocationId, …) ` .
586+ 2 . ** ` fetchAvailableOfframp ` ** sources the "Max" amount from the integrator's
587+ ` availableOfframp(user) ` view instead of the wallet balance.
588+ 3 . ** History** merges the user's per-user proxy (where ` order.user ` lives for
589+ offramps) via ` <PaymentHistory resolveExtraAddresses=…> ` .
590+
591+ ``` ts
592+ const TRADESTARS_ABI = [
593+ { name: " userStartOfframp" , type: " function" , stateMutability: " nonpayable" ,
594+ inputs: [
595+ { name: " allocationId" , type: " uint256" }, { name: " currency" , type: " bytes32" },
596+ { name: " fiatAmount" , type: " uint256" }, { name: " circleId" , type: " uint256" },
597+ { name: " preferredPaymentChannelConfigId" , type: " uint256" }, { name: " userPubKey" , type: " string" },
598+ ], outputs: [{ name: " orderId" , type: " uint256" }] },
599+ { name: " userDeliverOfframpUpi" , type: " function" , stateMutability: " nonpayable" ,
600+ inputs: [{ name: " orderId" , type: " uint256" }, { name: " encUpi" , type: " string" }], outputs: [] },
601+ { name: " syncOfframp" , type: " function" , stateMutability: " nonpayable" ,
602+ inputs: [{ name: " orderId" , type: " uint256" }], outputs: [] },
603+ { name: " availableOfframp" , type: " function" , stateMutability: " view" ,
604+ inputs: [{ name: " user" , type: " address" }], outputs: [{ type: " uint256" }] },
605+ { name: " pendingAllocations" , type: " function" , stateMutability: " view" ,
606+ inputs: [{ name: " user" , type: " address" }], outputs: [{ type: " uint256[]" }] },
607+ { name: " proxyAddress" , type: " function" , stateMutability: " view" ,
608+ inputs: [{ name: " user" , type: " address" }], outputs: [{ type: " address" }] },
609+ { type: " event" , name: " OfframpOrderPlaced" , inputs: [
610+ { name: " allocationId" , type: " uint256" , indexed: true },
611+ { name: " orderId" , type: " uint256" , indexed: true },
612+ { name: " user" , type: " address" , indexed: true },
613+ { name: " amount" , type: " uint256" , indexed: false } ] },
614+ ] as const ;
615+
616+ // Host picks an allocationId from pendingAllocations(user) before mounting <Cashout>.
617+ const allocationId = /* selected pending allocation (a bigint) */ ;
618+
619+ < Cashout
620+ /* …usdcAddress, diamondAddress, signer, currencies, subgraphUrl… */
621+ fetchAvailableOfframp = {(user) =>
622+ publicClient.readContract({ address : INTEGRATOR , abi : TRADESTARS_ABI ,
623+ functionName : " availableOfframp" , args : [user ] }) as Promise <bigint >}
624+ placeCashout = {async (ctx ) => { // NO approve — proxy already funded
625+ const data = encodeFunctionData ({ abi: TRADESTARS_ABI , functionName: " userStartOfframp" ,
626+ args: [allocationId , stringToHex (ctx .currency .symbol , { size: 32 }), 0n ,
627+ ctx .currency .circleId ! , ctx .currency .paymentChannelConfigId ?? 0n , ctx .userPubKey ] });
628+ const { hash } = await signer .sendTransaction ({ to: INTEGRATOR , data, gasLimit: 1_000_000 });
629+ const receipt = await publicClient .waitForTransactionReceipt ({ hash });
630+ return { orderId: parseTradeStarsOrderId (receipt ) /* OfframpOrderPlaced */ , txHash: hash };
631+ }}
632+ deliverUpi = {async (ctx ) => {
633+ const data = encodeFunctionData ({ abi: TRADESTARS_ABI , functionName: " userDeliverOfframpUpi" ,
634+ args: [BigInt (ctx .orderId ), ctx .encryptedUpi ] });
635+ const { hash } = await signer .sendTransaction ({ to: INTEGRATOR , data, gasLimit: 500_000 });
636+ await publicClient .waitForTransactionReceipt ({ hash });
637+ return { txHash: hash };
638+ }}
639+ reconcile = {async (ctx ) => { // permissionless syncOfframp
640+ const data = encodeFunctionData ({ abi: TRADESTARS_ABI , functionName: " syncOfframp" ,
641+ args: [BigInt (ctx .orderId )] });
642+ const { hash } = await signer .sendTransaction ({ to: INTEGRATOR , data, gasLimit: 200_000 });
643+ await publicClient .waitForTransactionReceipt ({ hash });
644+ return { txHash: hash };
645+ }}
646+ / >
647+
648+ // Offramps are attributed to the user's proxy — merge it into history:
649+ < PaymentHistory signer = {signer } subgraphUrl = {SUBGRAPH } usdcAddress = {USDC }
650+ resolveExtraAddresses = {async (user ) => [
651+ (await publicClient .readContract ({ address: INTEGRATOR , abi: TRADESTARS_ABI ,
652+ functionName: " proxyAddress" , args: [user ] })) as ` 0x${string } ` ,
653+ ]} / >
654+ ```
655+
656+ A cancelled offramp leaves the USDC in the user's proxy, so the ` <Cashout> `
657+ "Try again" button re-places from the same balance — self-serve retry, no
658+ relayer/owner. See ` payment-integrators/docs/OFFRAMP-V2.md ` for the contract side.
659+
578660> ** Tip:** ` userPubKey ` is auto-generated from the SDK's relay identity
579661> (lazily persisted in localStorage). Hosts that already use ` <Checkout> `
580662> share the same identity — no extra wiring required.
0 commit comments