Add a create-LOC form to your app
Outcome: your React app shows a ready-made form where a customer creates a LOC naming your business as the Beneficiary. You set the Beneficiary, the expiration, and the supported asset; the customer chooses the amount and signs the Sepolia transactions.
You need: React 19, package access, a Sepolia RPC URL, and a browser wallet funded with Sepolia test ETH and the supported test asset.
-
Set the Beneficiary, expiration, and reference fields.
This factory takes the expiration from your app’s config. It hides the Beneficiary and expiration fields, fixes creation to a Static LOC, limits the asset menu to the profile’s test USDC, and turns your tag schema into plain-language reference inputs:
import { createPublicClient, http } from 'viem';import { sepolia } from 'viem/chains';import { AnvilSDK, testnetProfile } from '@anvil/sdk/core';import type { LOCConfig } from '@anvil/sdk/core';const BENEFICIARY = '0x1111111111111111111111111111111111111111';const TESTNET_USDC = testnetProfile.contracts.USDC.address;export function createPartnerSDK({expiration,rpcUrl,}: {expiration: Date;rpcUrl: string;}) {const locConfig: LOCConfig = {beneficiary: { value: BENEFICIARY, hidden: true },expiration: { value: expiration, hidden: true },locBeneficiaryAllowList: [BENEFICIARY],staticLOCTokenAddressAllowlist: [TESTNET_USDC],createLOC: {staticOnly: true,// The form renders only the input segments. It assembles the fixed// and editable values in this order as// PAYMENT|<invoice>|<client reference>.tag: {type: 'pipe-delimited',fields: [{ key: 'event', type: 'fixed', value: 'PAYMENT' },{key: 'invoiceNumber',type: 'input',label: 'Invoice number',placeholder: 'INV-001',required: true,},{key: 'clientReference',type: 'input',label: 'Client reference',description: 'Use the reference from your payment request.',required: true,},],},},};return new AnvilSDK({publicClient: createPublicClient({chain: sepolia,transport: http(rpcUrl),}),profile: testnetProfile,locConfig,});}Replace
BENEFICIARYwith your business address. The customer never sees the fixed PAYMENT segment. The two input segments appear under Reference details, and with the values above the form assemblesPAYMENT|INV-001|CLIENT-42and submits it as unpadded UTF-8 bytes. The pipe separator is fixed so that every system reading a Beneficiary’s LOCs can split the same format.createLOC.staticOnlyis your integration’s policy rather than a starting form value. Set it totrueto offer only the one-asset Static form shown here. Set it tofalse, or leave it out, to offer the flexible two-asset form: matching collateral and credited assets use the Static workflow, and different assets use the Dynamic workflow. The customer sees one form either way, and restored form values leave the policy as you set it.Use a Static LOC when the Creator holds, or can hold, the credited token itself. It’s the simpler instrument, with no oracle price, no collateral factor to watch, and nothing to liquidate. In exchange, the full face value plus the withdrawal fee stays reserved in that token for the life of the LOC.
Use a Dynamic LOC when the Creator wants to keep collateral in a different asset from the one promised to the Beneficiary. That flexibility comes with price risk and the machinery that manages it: overcollateralization at creation, health measured against the pair’s liquidation threshold, and conversion when the price moves against the collateral.
When you render the page, pass the approved expiration and RPC URL from your app’s config. The expiration must fall within the current contract limits, and the form explains when it doesn’t.
-
Connect the customer’s wallet.
Add this function to the same module. If your app already uses wagmi, pass wagmi’s
WalletClienttosdk.updateWalletClient()instead.import { createWalletClient, custom } from 'viem';import 'viem/window';export async function connectWallet(sdk: AnvilSDK): Promise<void> {const provider = window.ethereum;if (!provider) throw new Error('No browser wallet found.');const [account] = await createWalletClient({chain: sepolia,transport: custom(provider),}).requestAddresses();if (!account) throw new Error('The wallet returned no account.');sdk.updateWalletClient(createWalletClient({account,chain: sepolia,transport: custom(provider),}));} -
Render the form.
AnvilProvidershares the SDK instance and a stable query client with the form. It reuses your app’s TanStackQueryClientProviderwhen there is one and supplies its own when there isn’t. The page shows your connect button until a wallet connects, then the configured Static creation flow.import { useMemo, useState } from 'react';import { AnvilProvider, CreateLOCFlow } from '@anvil/sdk';export function LetterOfCreditPage({expiration,rpcUrl,}: {expiration: Date;rpcUrl: string;}) {const sdk = useMemo(() => createPartnerSDK({ expiration, rpcUrl }),[expiration, rpcUrl]);const [connected, setConnected] = useState(false);return (<AnvilProvider sdk={sdk}>{connected ? (<CreateLOCFlow createOnly />) : (<buttononClick={() => {void connectWallet(sdk).then(() => setConnected(true));}}>Connect wallet</button>)}</AnvilProvider>);} -
Add the component stylesheet.
Import the published stylesheet once in your app’s main CSS file. It needs no Anvil design-token setup and no
node_modulesscanning.@import '@anvil/sdk/styles.css';
What your customer sees
- LOC Value — an amount input and a token menu limited to the assets you allow.
- Reference details — the editable fields your tag schema names.
- Connect wallet / Sign Transactions — the form asks for exactly the approvals, deposit, and creation transaction that account needs.
- LOC Created — a completion screen once the creation transaction confirms.
Your configuration sets the Beneficiary, the expiration, and the fixed reference segment, so the
form leaves them out. To show the Beneficiary or expiration as read-only, remove hidden: true from
that field. Reference details are stored publicly with the LOC, and the form tells the customer so;
keep secrets and sensitive personal data out of them.
Offer both Static and Dynamic LOCs
Set staticOnly: false at render time when a page should offer both workflows:
import { CreateLOCForm as FlexibleForm } from '@anvil/sdk';
export function FlexibleCreateLOCForm() { return <FlexibleForm config={{ staticOnly: false }} />;}When the customer picks the same token for both, the form creates a Static LOC. The credited value is the one editable amount, and the collateral row keeps its token selector and balance while showing, read-only, the collateral required including the fee. A tooltip explains why that amount follows the credited value. When the customer picks different tokens, a separate editable collateral amount appears and the form creates a Dynamic LOC. Either way, only the collateral row shows the customer’s available balance: the credited value is what the Beneficiary can redeem, and the Creator doesn’t contribute it as a separate asset.
What the SDK handles, and what your app handles
- The SDK applies your rules and runs the steps. It applies the Beneficiary, expiration, and
asset rules from
LOCConfig, validates the proposed LOC before building a workflow, works out which approval, vault deposit, signature, and create steps are needed, and runs them in order, reporting progress and failures in the form. - Your app supplies the business terms and the wallet. It provides the Beneficiary address, the approved expiration, and the RPC URL, connects the customer’s wallet, funds the test account, and decides how the page fits into your product and what happens after completion.
Customize the form
The same LOCConfig can:
- show a pinned
beneficiaryorexpirationas read-only instead of hiding it; - allow more than one Beneficiary with
locBeneficiaryAllowList; - offer more supported Static LOC assets with
staticLOCTokenAddressAllowlist; - define any ordered mix of fixed and editable tag segments with stable
keyvalues.
Each segment’s key is its durable identifier, so you can change labels and help text freely
without affecting saved values. Tag segments follow a few rules:
- Keys are unique, start with a letter, and contain only letters, numbers, underscores, or hyphens.
- Labels are nonblank.
- Fixed values, defaults, and entered values are printable ASCII without
|. requireddefaults to false, and a required value can’t be whitespace only.
The form checks required fields and the bound deployment’s byte limit for the assembled tag before
it builds a workflow. Raw hex values and submission-context resolvers still work when you’d rather
the SDK didn’t render reference inputs, and setting a render-level tag to null turns off a
provider schema for one form.
These settings guard what your integration submits. They aren’t onchain access controls: a direct
contract call goes around them. LOCConfig has
the exact contract.
Run the workflow yourself
CreateLOCFlow runs the workflow for you. If you render CreateLOCForm directly instead, your host
runs it: execute the prepared workflow through useWorkflowLifecycle, and keep the form’s callback
values with that run.
- Read the outcome from
settlement. A run reports failure assettlementstate, even when every operation is still queued, so render its message rather than reading failure from operation statuses. - Send invalid terms back to the form. A
preflight-invalidsettlement returns its issues through the form’sissuesprop. Other failures belong in your own banner. - Handle a send that may have landed. A
wallet-indeterminatesettlement means a transaction may already be onchain. ShowUnconfirmedTransactionin place of the run, withgetUnconfirmedSendsupplying its wording, rather than returning the customer to a live submit button. - Restore the form.
values.tagis the complete assembled tag, andvalues.tagValuesholds only the editable segments. Passing them back throughformValuesrestores the same invoice and client references.
Some runs settle nothing at all. An account change, an interruption mid-signing, or a run paused on
an Indeterminate operation that still has a transaction hash to re-check all leave settlement
undefined for good. (A send that failed in transit is different: it settles as
wallet-indeterminate.) So give the customer a way out that doesn’t wait on a settlement; a
recovery action that only appears after one would leave them stuck on the run:
import { useState } from 'react';
import { CreateLOCForm, getUnconfirmedSend, getWorkflowSettlementMessage, UnconfirmedTransaction, useWorkflowLifecycle, WorkflowComplete, WorkflowView,} from '@anvil/sdk';
import type { CreateLOCFormValues, WorkflowSettlement } from '@anvil/sdk';import type { WalletOperationWorkflow } from '@anvil/sdk/core';
type CreateLOCRun = { workflow: WalletOperationWorkflow; values: CreateLOCFormValues;};
type FailedWorkflowSettlement = Exclude< WorkflowSettlement, { status: 'succeeded' }>;
function CreateLOCExecution({ run, onFailed, onAbandon, onDone,}: { run: CreateLOCRun; onFailed: (settlement: FailedWorkflowSettlement) => void; onAbandon: () => void; onDone: () => void;}) { const { operations, settlement } = useWorkflowLifecycle(run.workflow);
if (settlement?.status === 'succeeded') { return <WorkflowComplete title="LOC Created" onDone={onDone} />; }
// A send whose outcome is unknown may already be on chain. It gets the // recovery screen rather than a one-line error beside a way back to a // live submit button. Pass `explorer` and `address` to add the account // link. const unconfirmedSend = getUnconfirmedSend(settlement); if (unconfirmedSend) { return ( <UnconfirmedTransaction {...unconfirmedSend} // Fires only once the user has checked and says the send did not // land, so the form returns with its values and no failure banner. onRetry={onAbandon} /> ); }
if (settlement) { const message = getWorkflowSettlementMessage(settlement); return ( <div> <p role="alert">{message}</p> <button onClick={() => onFailed(settlement)}>Back to form</button> </div> ); }
// Still running -- or settled nothing at all, which is terminal for this // host: an account change or an interruption mid-signing, and a run // parked on a confirmation timeout it can still re-check, all leave // `settlement` undefined for good. (A send that failed in transit // settles as `wallet-indeterminate` and gets the screen above.) // Nothing else clears `run`, so the escape must not be gated on a // settlement that may never arrive. return ( <div> <WorkflowView operations={operations} /> <button onClick={onAbandon}>Abandon this run</button> </div> );}
export function RecoverableCreateLOCForm() { const [run, setRun] = useState<CreateLOCRun>(); const [formValues, setFormValues] = useState<CreateLOCFormValues>(); const [settlement, setSettlement] = useState<FailedWorkflowSettlement>();
if (run) { return ( <CreateLOCExecution run={run} onFailed={failed => { setFormValues(run.values); setSettlement(failed); setRun(undefined); }} onAbandon={() => { // Keep the values, but report no failure: an abandoned run // reached no verdict, so there is nothing to tell the user went // wrong. setFormValues(run.values); setSettlement(undefined); setRun(undefined); }} onDone={() => { setFormValues(undefined); setSettlement(undefined); setRun(undefined); }} /> ); }
const invalidSettlement = settlement?.status === 'preflight-invalid' ? settlement : undefined; const issues = invalidSettlement?.issues; const message = settlement?.status === 'preflight-invalid' ? undefined : getWorkflowSettlementMessage(settlement);
return ( <div> {message && <p role="alert">{message}</p>} <CreateLOCForm {...(formValues === undefined ? {} : { formValues })} {...(issues === undefined ? {} : { issues })} onPrepareWorkflow={(workflow, values) => { // Keep tag bytes and editable segments with this run. setRun({ workflow, values }); setSettlement(undefined); }} /> </div> );}Need your own interface?
Use useCreateLOCWorkflow from
@anvil/sdk/react. It runs the same validation and builds the same workflow while your components
own the presentation. If you render your own reference-data counter, read the limit from
sdk.loc.getCreateTagByteLimit() so it follows the deployment profile.
Browse the React APIs →
Moving to mainnet
When your integration is ready, use mainnetProfile with viem’s mainnet chain and a mainnet RPC.
The Beneficiary, expiration, asset policy, component, and workflow stay the same. Verify every
address and supported asset again before using real collateral.
Verify the result
Read the LOC back from the contract with sdk.loc.getOutstandingLetterOfCredit({ id }).
Continue with the read walkthrough →