diff --git a/src/commerce/CommercePayButton.stories.tsx b/src/commerce/CommercePayButton.stories.tsx new file mode 100644 index 0000000000..552fd5a18c --- /dev/null +++ b/src/commerce/CommercePayButton.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import {CommercePayButton} from './CommercePayButton' + +const meta = { + title: 'Commerce Pay Button', + component: CommercePayButton, + args: { + chargeId: "a0a94171-4b24-4097-bc52-7931f9ede42d" + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/src/commerce/CommercePayButton.tsx b/src/commerce/CommercePayButton.tsx new file mode 100644 index 0000000000..ff6a17f357 --- /dev/null +++ b/src/commerce/CommercePayButton.tsx @@ -0,0 +1,108 @@ +import { connect } from "@wagmi/core"; +import { getCallsStatus, sendCalls } from "@wagmi/core/experimental"; +import { coinbaseWallet } from "wagmi/connectors"; +import { smartWalletConfig } from "./smartWalletConfig"; +import { hydrateCommerceCharge } from "../network/commerce/hydrateCommereCharge"; +import { useEffect, useState } from "react"; +import { getCommerceCharge } from "../network/commerce/getCommerceCharge"; +import { Web3Charge } from "../network/commerce/types/Web3Charge"; +import { base } from "viem/chains"; +import { getCommerceCallData } from "./utils/getCommerceCallData"; + +type CommercePayButtonProps = { + chargeId: string; +}; + +const SMART_WALLET_CONNECTOR = coinbaseWallet({ + preference: "smartWalletOnly", +}); + +const BASE_COMMERCE_URL = "https://api.commerce.coinbase.com"; + +export function CommercePayButton({ chargeId }: CommercePayButtonProps) { + const [charge, setCharge] = useState(); + const [transactionCallsId, setTransactionCallsId] = useState(""); + const [transactionHash, setTransactionHash] = useState(""); + + useEffect(() => { + async function checkCallsStatus() { + const { status, receipts } = await getCallsStatus(smartWalletConfig, { + id: transactionCallsId, + }); + if (status === "CONFIRMED") { + const transactionHash = receipts?.[0].transactionHash; + if (transactionHash) { + setTransactionHash(transactionHash); + } + } + } + const interval = setInterval(() => { + if (transactionCallsId && !transactionHash) { + void checkCallsStatus(); + } + }, 2000); + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [transactionCallsId]); + + useEffect(() => { + async function loadCharge() { + const chargeResponse = await getCommerceCharge( + BASE_COMMERCE_URL, + chargeId + ); + console.log({ chargeResponse }); + setCharge(chargeResponse.data); + } + if (!charge) { + void loadCharge(); + } + }, [chargeId]); + + const handlePayment = async () => { + const { accounts } = await connect(smartWalletConfig, { + connector: SMART_WALLET_CONNECTOR, + chainId: base.id, + }); + const senderAddress = accounts[0]; + if (!senderAddress) { + return; + } + const { data: hydratedCharge } = await hydrateCommerceCharge( + BASE_COMMERCE_URL, + chargeId, + senderAddress + ); + const { tokenApprovalCall, transferTokenPreApprovedCall } = + getCommerceCallData(hydratedCharge); + + const callsId = await sendCalls(smartWalletConfig, { + calls: [tokenApprovalCall, transferTokenPreApprovedCall], + }); + setTransactionCallsId(callsId); + }; + if (!charge) { + return
Loading...
; + } + if (!transactionHash && transactionCallsId) { + return
Processing...
; + } + if (transactionHash) { + return ( +
+
Payment Complete
+ + View on Block Explorer + +
+ ); + } + return ( +
+ +
+ ); +} diff --git a/src/commerce/smartWalletConfig.ts b/src/commerce/smartWalletConfig.ts new file mode 100644 index 0000000000..8e1c796f45 --- /dev/null +++ b/src/commerce/smartWalletConfig.ts @@ -0,0 +1,11 @@ +import { createConfig, http } from 'wagmi' +import { base } from 'wagmi/chains' +import { injected } from 'wagmi/connectors' + +export const smartWalletConfig = createConfig({ + chains: [base], + connectors: [injected()], + transports: { + [base.id]: http(), + }, +}) \ No newline at end of file diff --git a/src/commerce/utils/TransfersContractAbi.ts b/src/commerce/utils/TransfersContractAbi.ts new file mode 100644 index 0000000000..6e3092e92b --- /dev/null +++ b/src/commerce/utils/TransfersContractAbi.ts @@ -0,0 +1,817 @@ +export const contractAbi = [ + { + inputs: [ + { + internalType: "contract IUniversalRouter", + name: "_uniswap", + type: "address", + }, + { internalType: "contract Permit2", name: "_permit2", type: "address" }, + { internalType: "address", name: "_initialOperator", type: "address" }, + { + internalType: "address", + name: "_initialFeeDestination", + type: "address", + }, + { + internalType: "contract IWrappedNativeCurrency", + name: "_wrappedNativeCurrency", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "AlreadyProcessed", type: "error" }, + { inputs: [], name: "ExpiredIntent", type: "error" }, + { + inputs: [ + { internalType: "address", name: "attemptedCurrency", type: "address" }, + ], + name: "IncorrectCurrency", + type: "error", + }, + { inputs: [], name: "InexactTransfer", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "difference", type: "uint256" }], + name: "InsufficientAllowance", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "difference", type: "uint256" }], + name: "InsufficientBalance", + type: "error", + }, + { + inputs: [{ internalType: "int256", name: "difference", type: "int256" }], + name: "InvalidNativeAmount", + type: "error", + }, + { inputs: [], name: "InvalidSignature", type: "error" }, + { inputs: [], name: "InvalidTransferDetails", type: "error" }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "bool", name: "isRefund", type: "bool" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "NativeTransferFailed", + type: "error", + }, + { inputs: [], name: "NullRecipient", type: "error" }, + { inputs: [], name: "OperatorNotRegistered", type: "error" }, + { + inputs: [{ internalType: "bytes", name: "reason", type: "bytes" }], + name: "SwapFailedBytes", + type: "error", + }, + { + inputs: [{ internalType: "string", name: "reason", type: "string" }], + name: "SwapFailedString", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "feeDestination", + type: "address", + }, + ], + name: "OperatorRegistered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "operator", + type: "address", + }, + ], + name: "OperatorUnregistered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { indexed: false, internalType: "bytes16", name: "id", type: "bytes16" }, + { + indexed: false, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "spentAmount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "spentCurrency", + type: "address", + }, + ], + name: "Transferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "permit2", + outputs: [{ internalType: "contract Permit2", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "registerOperator", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_feeDestination", type: "address" }, + ], + name: "registerOperatorWithFeeDestination", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newSweeper", type: "address" }], + name: "setSweeper", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct EIP2612SignatureTransferData", + name: "_signatureTransferData", + type: "tuple", + }, + ], + name: "subsidizedTransferToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { internalType: "uint24", name: "poolFeesTier", type: "uint24" }, + ], + name: "swapAndTransferUniswapV3Native", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { + components: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { + internalType: "uint256", + name: "requestedAmount", + type: "uint256", + }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct Permit2SignatureTransferData", + name: "_signatureTransferData", + type: "tuple", + }, + { internalType: "uint24", name: "poolFeesTier", type: "uint24" }, + ], + name: "swapAndTransferUniswapV3Token", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { internalType: "address", name: "_tokenIn", type: "address" }, + { internalType: "uint256", name: "maxWillingToPay", type: "uint256" }, + { internalType: "uint24", name: "poolFeesTier", type: "uint24" }, + ], + name: "swapAndTransferUniswapV3TokenPreApproved", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address payable", name: "destination", type: "address" }, + ], + name: "sweepETH", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address payable", name: "destination", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "sweepETHAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_token", type: "address" }, + { internalType: "address", name: "destination", type: "address" }, + ], + name: "sweepToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_token", type: "address" }, + { internalType: "address", name: "destination", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "sweepTokenAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "sweeper", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + ], + name: "transferNative", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { + components: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { + internalType: "uint256", + name: "requestedAmount", + type: "uint256", + }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct Permit2SignatureTransferData", + name: "_signatureTransferData", + type: "tuple", + }, + ], + name: "transferToken", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + ], + name: "transferTokenPreApproved", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unregisterOperator", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + { + components: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { + internalType: "uint256", + name: "requestedAmount", + type: "uint256", + }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + internalType: "struct Permit2SignatureTransferData", + name: "_signatureTransferData", + type: "tuple", + }, + ], + name: "unwrapAndTransfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + ], + name: "unwrapAndTransferPreApproved", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint256", name: "recipientAmount", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { + internalType: "address payable", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "recipientCurrency", + type: "address", + }, + { + internalType: "address", + name: "refundDestination", + type: "address", + }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, + { internalType: "bytes16", name: "id", type: "bytes16" }, + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + { internalType: "bytes", name: "prefix", type: "bytes" }, + ], + internalType: "struct TransferIntent", + name: "_intent", + type: "tuple", + }, + ], + name: "wrapAndTransfer", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +] as const; diff --git a/src/commerce/utils/getCommerceCallData.ts b/src/commerce/utils/getCommerceCallData.ts new file mode 100644 index 0000000000..9e806d1406 --- /dev/null +++ b/src/commerce/utils/getCommerceCallData.ts @@ -0,0 +1,62 @@ +import { encodeFunctionData, erc20Abi, parseUnits } from "viem"; +import { base } from "viem/chains"; +import { Web3Charge } from "../../network/commerce/types/Web3Charge"; +import { contractAbi } from "./TransfersContractAbi"; + +const USDC_DECIMALS = 6; + +export function getCommerceCallData(charge: Web3Charge) { + const { + web3_data: { + contract_addresses, + transfer_intent: { + call_data: { + deadline, + recipient_amount, + recipient, + recipient_currency, + fee_amount, + refund_destination, + operator, + signature, + prefix, + id, + }, + }, + }, + } = charge; + return { + tokenApprovalCall: { + to: recipient_currency, + data: encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [ + contract_addresses[base.id], + parseUnits(charge.pricing.settlement.amount, USDC_DECIMALS), + ], + }), + }, + transferTokenPreApprovedCall: { + to: contract_addresses["8453"], + data: encodeFunctionData({ + abi: contractAbi, + functionName: "transferTokenPreApproved", + args: [ + { + id, + recipientAmount: BigInt(recipient_amount), + deadline: BigInt(Math.floor(new Date(deadline).getTime() / 1000)), + recipient, + recipientCurrency: recipient_currency, + refundDestination: refund_destination, + feeAmount: BigInt(fee_amount), + operator, + signature, + prefix, + }, + ], + }), + }, + }; +} diff --git a/src/network/commerce/FetchError.ts b/src/network/commerce/FetchError.ts new file mode 100644 index 0000000000..e37aecb944 --- /dev/null +++ b/src/network/commerce/FetchError.ts @@ -0,0 +1,6 @@ +export class FetchError extends Error { + constructor(message: string) { + super(message); + this.name = "CommerceFetchError"; + } +} diff --git a/src/network/commerce/getCommerceCharge.ts b/src/network/commerce/getCommerceCharge.ts new file mode 100644 index 0000000000..ca525c98a2 --- /dev/null +++ b/src/network/commerce/getCommerceCharge.ts @@ -0,0 +1,23 @@ +import { FetchError } from "./FetchError"; +import { Web3Charge } from "./types/Web3Charge"; + +export async function getCommerceCharge( + baseUrl: string, + chargeId: string, +) { + const options = { + method: 'GET', + url: `${baseUrl}/charges/${chargeId}`, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + }; + const resp = await fetch(options.url, options); + if (resp.status !== 200) { + throw new FetchError( + `non-200 status returned from neynar : ${resp.status}`, + ); + } + return await resp.json() as { data: Web3Charge }; +} diff --git a/src/network/commerce/hydrateCommereCharge.ts b/src/network/commerce/hydrateCommereCharge.ts new file mode 100644 index 0000000000..e44731c225 --- /dev/null +++ b/src/network/commerce/hydrateCommereCharge.ts @@ -0,0 +1,29 @@ +import { FetchError } from "./FetchError"; +import { Web3Charge } from "./types/Web3Charge"; + +export async function hydrateCommerceCharge( + baseUrl: string, + chargeId: string, + senderAddress: string, + +) { + const options = { + method: 'PUT', + url: `${baseUrl}/charges/${chargeId}/hydrate`, + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + chain_id: 8453, + sender: senderAddress + }) + }; + const resp = await fetch(options.url, options); + if (resp.status !== 200) { + throw new FetchError( + `non-200 status returned from neynar : ${resp.status}`, + ); + } + return await resp.json() as { data: Web3Charge }; +} diff --git a/src/network/commerce/types/Web3Charge.ts b/src/network/commerce/types/Web3Charge.ts new file mode 100644 index 0000000000..39a4d9e1e2 --- /dev/null +++ b/src/network/commerce/types/Web3Charge.ts @@ -0,0 +1,144 @@ +export type Web3Charge = { + charge_kind: Web3ChargeChargeKind; + checkout?: Web3ChargeCheckout; + code: string; + description?: string; + expires_at: string; + hosted_url: string; + id: string; + metadata?: Web3ChargeMetadata; + name?: string; + organization_name?: string; + pricing: Web3ChargePricing; + pricing_type: Web3ChargePricingType; + pwcb_only?: boolean; + redirects?: Web3ChargeRedirects; + support_email: string; + third_party_provider?: string; + timeline: Web3ChargeTimelineItem[]; + web3_data: Web3ChargeWeb3Data; +}; + +export type Web3ChargeWeb3DataSubsidizedPaymentsChainToTokens = { + [key: string]: any; +}; + +export type Web3ChargeWeb3DataSettlementCurrencyAddresses = { + [key: string]: string; +}; + +export type Web3ChargeWeb3DataContractAddresses = { [key: string]: `0x${string}` }; + +export type Web3ChargeWeb3Data = { + failure_events: Web3ChargeWeb3DataFailureEventsItem[]; + success_events: Web3ChargeWeb3DataSuccessEventsItem[]; + transfer_intent: Web3ChargeWeb3DataTransferIntent; + contract_addresses: Web3ChargeWeb3DataContractAddresses; + settlement_currency_addresses?: Web3ChargeWeb3DataSettlementCurrencyAddresses; +}; + +export type Web3ChargeWeb3DataTransferIntentMetadata = { + chain_id: number; + /** @deprecated */ + contract_address: string; + sender: string; +}; + +export type Web3ChargeWeb3DataTransferIntentCallData = { + deadline: string; + fee_amount: string; + id: `0x${string}`; + operator: `0x${string}`; + prefix: `0x${string}`; + recipient: `0x${string}`; + recipient_amount: string; + recipient_currency: `0x${string}`; + refund_destination: `0x${string}`; + signature: `0x${string}`; +}; + +export type Web3ChargeWeb3DataTransferIntent = { + call_data: Web3ChargeWeb3DataTransferIntentCallData; + metadata: Web3ChargeWeb3DataTransferIntentMetadata; +}; + +export type Web3ChargeWeb3DataSuccessEventsItem = { + chain_id: number; + finalized: boolean; + input_token_address: string; + input_token_amount: string; + network_fee_paid: string; + network_fee_paid_local?: string; + recipient: string; + sender: string; + timestamp: string; + tx_hsh: string; +}; + +export type Web3ChargeWeb3DataFailureEventsItem = { + chain_id?: number; + input_token_address?: string; + network_fee_paid?: string; + reason?: string; + sender?: string; + timestamp?: string; + tx_hsh?: string; +}; + +export type Web3ChargeTimelineItemStatus = + (typeof Web3ChargeTimelineItemStatus)[keyof typeof Web3ChargeTimelineItemStatus]; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const Web3ChargeTimelineItemStatus = { + COMPLETED: "COMPLETED", + EXPIRED: "EXPIRED", + FAILED: "FAILED", + NEW: "NEW", + PENDING: "PENDING", + SIGNED: "SIGNED", + CANCELED: "CANCELED", +} as const; + +export type Web3ChargeTimelineItem = { + status: Web3ChargeTimelineItemStatus; + time: string; +}; + +export type Web3ChargeRedirects = { + cancel_url?: string; + success_url?: string; + will_redirect_after_success?: boolean; +}; + +export type Web3ChargePricingType = + (typeof Web3ChargePricingType)[keyof typeof Web3ChargePricingType]; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const Web3ChargePricingType = { + fixed_price: "fixed_price", + no_price: "no_price", +} as const; + +export type Web3ChargePricing = { + local: Currency; + settlement: Currency; +}; + +export type Web3ChargeMetadata = { [key: string]: string }; + +export type Web3ChargeCheckout = { + id?: string; +}; + +export type Web3ChargeChargeKind = + (typeof Web3ChargeChargeKind)[keyof typeof Web3ChargeChargeKind]; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const Web3ChargeChargeKind = { + WEB3: "WEB3", +} as const; + +export type Currency = { + amount: string; + currency: string; +};