Skip to content

Read a LOC

Every Letter of Credit lives onchain, so anyone can verify one. No wallet, no signature. The SDK gives you two reads:

  • The full record. sdk.loc.getLetterOfCredit returns who created it, who it’s for, what it promises, and everything that has happened to it since, including after it’s redeemed or canceled.
  • The live position. sdk.loc.getOutstandingLetterOfCredit reads what the Beneficiary can still redeem, straight from the contract. Use it right before you act on a LOC.

Outcome: you can look up any LOC by its ID and show what it promises and where it stands.

You need: Node.js 24, package access, and an RPC endpoint. The full record also reads the subgraph, which your profile points at; the live position needs only RPC.

Both reads identify a LOC by a portable { chainId, contractAddress, id } reference. A configured SDK also accepts { id } alone and fills in its profile’s chain and LOC contract.

Read the full record

import { createPublicClient, http } from 'viem';
import { sepolia } from 'viem/chains';
import { AnvilSDK, testnetProfile } from '@anvil/sdk/core';
const sdk = new AnvilSDK({
publicClient: createPublicClient({
chain: sepolia,
transport: http('https://your-rpc-url'),
}),
// A generated environment profile supplies the protocol-managed
// config — contract addresses and the subgraph URL — for that
// environment. Import one statically (`testnetProfile`,
// `mainnetProfile`) to tree-shake the rest, or select from
// `anvilProfiles` by name at runtime. An explicit `subgraphUrl` may
// override the profile's indexed-data endpoint without separating any
// contract address from its ABI.
profile: testnetProfile,
});
const loc = await sdk.loc.getLetterOfCredit({ id: 28n });
console.log(loc);

The record groups facts by what changes them:

FieldWhat it holds
referenceChain, LOC contract, and numeric ID.
originFixed at creation: Creator, Beneficiary, original token amounts, creation kind, initial expiration, tag, and the creation transaction.
creationContextThe token pair’s terms when the LOC was created, where the LOC has a pair. Current risk terms are a separate read.
lifecycleRedemption totals, liquidation history, the outcome of a full conversion, and how the LOC ended.
outstandingRemaining amounts and the reserved or converted collateral behind them, or null once the LOC is resolved.
indexedAtThe subgraph deployment, block hash, block number, and timestamp every fact in this read comes from.

A redeemed or canceled LOC keeps its full record, so its origin and lifecycle stay readable. An ID that was never created returns undefined.

When the subgraph can’t serve the current LOC model, because it runs an older schema or hasn’t finished indexing, the read throws SubgraphCompatibilityError. That’s a different answer from “no such LOC”, so report it to the host instead of showing an empty result:

import { SubgraphCompatibilityError } from '@anvil/sdk/core';
import type { AnvilSDK } from '@anvil/sdk/core';
export async function readLOCWithCompatibility(sdk: AnvilSDK, id: bigint) {
try {
const loc = await sdk.loc.getLetterOfCredit({ id });
if (loc === undefined) {
console.log('No LOC with this ID exists at the indexed boundary.');
}
return loc;
} catch (error) {
if (error instanceof SubgraphCompatibilityError) {
// Report incompatibility separately from an absent LOC.
console.error(
'The index cannot serve this LOC model:',
error.reason
);
}
throw error;
}
}

Example result

This type-checked fixture shows a Static LOC that is still open, backed by 100.5 test USDC for a 100 USDC face value. The LetterOfCredit, Collateral Vault, and USDC addresses and the subgraph deployment are the public testnet’s. The parties, transaction hashes, and amounts are illustrative rather than a captured response, so your endpoint returns your own values.

export const exampleLOC = {
reference: {
chainId: 11155111n,
contractAddress: '0xcd327c5bc321F5CaFB6e11D3e762531549Bb4e9f',
id: 28n,
},
origin: {
reference: {
chainId: 11155111n,
contractAddress: '0xcd327c5bc321F5CaFB6e11D3e762531549Bb4e9f',
id: 28n,
},
creator: '0xb120AaE0dF154cE4eF41F04FC39052dBb063f72A',
beneficiary: '0x037937ea830616E7a6e75434Efcd6B2171d279Ed',
creationKind: 'static',
collateralContractAddress: collateralVault,
originalCollateral: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 100_500_000n,
},
originalClaimableCollateral: 100_000_000n,
credited: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 100_000_000n,
},
initialExpirationTimestamp: 1_797_658_932n,
originatingCollateralReservation: {
chainId: 11155111n,
collateralContractAddress: collateralVault,
id: 39n,
},
createdIn: {
hash: creationHash,
sender: '0xb120AaE0dF154cE4eF41F04FC39052dBb063f72A',
blockNumber: 11_741_203n,
blockHash: creationBlockHash,
timestamp: 1_789_882_932n,
transactionIndex: 37n,
},
tag: null,
},
creationContext: { kind: 'notApplicable' },
lifecycle: {
redemption: {
count: 0,
totalCredited: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 0n,
},
},
liquidation: {
count: 0,
totalCollateralConverted: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 0n,
},
totalLiquidatorFees: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 0n,
},
fullConversion: null,
},
resolution: null,
},
outstanding: {
reference: {
chainId: 11155111n,
contractAddress: '0xcd327c5bc321F5CaFB6e11D3e762531549Bb4e9f',
id: 28n,
},
creator: '0xb120AaE0dF154cE4eF41F04FC39052dBb063f72A',
beneficiary: '0x037937ea830616E7a6e75434Efcd6B2171d279Ed',
creationKind: 'static',
remainingCredited: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 100_000_000n,
},
expirationTimestamp: 1_797_658_932n,
backing: {
kind: 'reserved',
reservation: {
chainId: 11155111n,
collateralContractAddress: collateralVault,
id: 39n,
},
collateral: {
tokenAddress: '0x40181850236273444880bf1EdFb111D151788956',
amount: 100_500_000n,
},
claimableCollateral: 100_000_000n,
},
},
indexedAt: {
deployment: 'QmUR1xnhjZf9yZ2dREjmf6cTo3iey26mgXqmWegQkor5gj',
blockHash: indexedBlockHash,
blockNumber: 11_741_205n,
timestamp: 1_789_882_956n,
},
} satisfies LetterOfCredit;

Read the live position

const loc = await sdk.loc.getOutstandingLetterOfCredit({ id: 42n });
if (loc === undefined) {
// Fully redeemed or canceled LOCs are purged from contract state.
console.log('LOC 42 is no longer outstanding');
return;
}
const expires = ethereumTimestampToDate(loc.expirationTimestamp);
const { amount, tokenAddress } = loc.remainingCredited;
console.log(
`${amount} ${tokenAddress} owed to ` +
`${loc.beneficiary}, expires ${expires.toISOString()}`
);

The live position holds the remaining credited amount, the current expiration, and the backing behind it, which takes one of two forms:

  • Reserved: collateral still held in the Collateral Vault, with the reservation’s chain, vault, and ID, the collateral token and amount, and how much of it the LOC contract can claim.
  • Converted: an amount already in the credited token, after a conversion.

Once a LOC expires, the Beneficiary can no longer redeem it, and any account can cancel it. Canceling releases the full collateral position back to the Creator at once; until someone does, the expired LOC stays outstanding. Once a LOC is fully redeemed or canceled, the contract removes it, so the read returns undefined, exactly as it does for an ID that never existed.

Token amounts arrive in the token’s smallest units, alongside the token address. Token metadata is a separate read, sdk.tokens.getToken({ address }). If metadata can’t be fetched, show the address and base units as they are: guessing decimals, hiding the position, or filtering it against today’s creation catalog would all misreport it. Current pair settings and pair prices are separate reads as well.

The LOC lifecycle

A LOC’s condition comes from its stored amounts, its expiration timestamp, and its events rather than from a single status field. A LOC is expired from the moment the block time reaches its expiration timestamp. Expiration changes which operations are allowed, and the LOC stays in contract storage until it is fully redeemed or canceled.

The SDK keeps these conditions as separate dimensions, because several can be true at once:

DimensionFacts
OriginCreation kind, parties, assets, initial amounts, and transaction, fixed at creation.
RedemptionHow many redemptions, and the total credited tokens redeemed.
LiquidationPartial or full conversion, the collateral converted, fees, and any shortfall observed.
ResolutionRedeemed or canceled, with the transaction; otherwise unresolved.
OutstandingRemaining credited amount, current expiration, and reserved or converted backing, until resolution.
TimeUnexpired or expired by the caller’s clock, for a LOC that is still outstanding.

A LOC can be converted, partly redeemed, and expired all at once. Its conversion history stays after expiration, and an observed conversion shortfall stays in the record after the LOC is resolved.

Derive status and inspect history

const loc = await sdk.loc.getLetterOfCredit({ id: 42n });
if (!loc) return;
const now = BigInt(Math.floor(Date.now() / 1000));
const status = deriveLetterOfCreditStatus(loc, now);
console.log(
loc.origin.credited,
loc.lifecycle.resolution,
status.displayStatus
);
console.log(loc.indexedAt.deployment, loc.indexedAt.blockHash);
// No caller selected: every direct capability is false.
const capabilities = deriveLetterOfCreditDirectCapabilities(
loc,
status,
null,
null
);
console.log(capabilities);

deriveLetterOfCreditStatus gives you one display label for a list or a badge. Because a LOC can be in several conditions at once, the label keeps only the most important one: a converted LOC can also be expired, and a conversion shortfall still counts as historical insolvency after a later redemption or cancellation. Use the lifecycle facts for accounting, history, and detail views.

deriveLetterOfCreditDirectCapabilities takes the account you pass and, optionally, a current risk assessment, and tells you which operations that account can start. Each operation still validates against fresh state before anything is signed.

Good to know

  • Some of the earliest LOCs have no reservation link in the subgraph. For some LOCs created under the first contract release, the subgraph can’t recover which Collateral Vault reservation backs them, so reservation is null in their indexed outstanding and history backing. Their amounts and lifecycle read normally. The live position from the contract always carries the reservation, and operations use that.
  • A custom subgraphUrl must serve the current schema, fully indexed. Otherwise reads throw SubgraphCompatibilityError as described above.

Monitor LOCs covers complete collections, current risk, reservations, and paginated history for each LOC. Where data comes from explains what the subgraph has indexed and when the chain confirms it. Next: validate before signing.