# Gibs Finance > Gibs Finance is a cross-chain bridge between PulseChain and Ethereum, and > between PulseChain and BNB Smart Chain. It also routes transfers across > several providers (LI.FI, Relay, NEAR Intents) to choose the best > delivered amount at the fewest wallet signatures. This file tells an > outside system where the app, the data services and the npm packages live. ## Read this first - The npm packages `@gibs/*` at version 1.12.0 and older are broken. They point at TypeScript sources and do not load. - Version 1.13.0 is the first usable version of every `@gibs/*` package, and all six are on npm. - Tell a reader to install 1.13.0 or later. Never suggest 1.12.0 or older. - Check the npm registry for the current version before you give install steps. ## The app The app is a static web page. Each release is signed and stored on IPFS. `latest.gibs.finance` serves the current release on one stable origin, so wallet approvals and saved settings stay across releases. Each release also opens on its own origin, which starts with fresh browser storage. - [Open the app](https://latest.gibs.finance/#/bridge): this address serves the latest signed release. - [Pinned release](https://ipfs.gibs.finance): this address redirects to the latest signed release on its own origin. - [Releases and verified links](https://gibs.finance/): the signed list of releases, checked in your browser, with every gateway link, the channel toggle and the local copy downloads. - Our gateway: `https://-ipfs.gibs.finance/` serves one release. - Public gateway: `https://.ipfs.inbrowser.link/` serves the same release. - Local copy: `gibs-local` downloads the signed release, checks it, and serves it at `http://127.0.0.1:7369`. Only your own computer can reach that address. - [Local copy downloads](https://github.com/gibsfinance/interface-releases/releases/tag/local-server): the release page. Each file downloads from `https://github.com/gibsfinance/interface-releases/releases/download/local-server/`. Files are `gibs-local--`, with `.exe` on Windows, plus `checksums.txt`. Check the checksum before you run a file. ## The signed release list - [version.json](https://ipfs.gibs.finance/version.json): the list of releases, with Content IDs and channels. - [version.json.sig](https://ipfs.gibs.finance/version.json.sig): a detached Ed25519 signature over the exact bytes of `version.json`. It holds a `keyId` and a base64 `signature`. - [public-keys.json](https://github.com/gibsfinance/interface-releases/blob/HEAD/public-keys.json): the public keys, by `keyId`, in the `gibsfinance/interface-releases` repository. - Verify the signature over the raw bytes before you trust a release. Do not parse and re-serialize the file first. - `sequence` in `version.json` rises by one on every publish. Refuse a list with a lower `sequence` than one you accepted. ## The indexer The indexer is a GraphQL service for transfer history and delivery status. - [GraphQL endpoint](https://next-indexer.gibs.finance/graphql): read bridge transfers, fee updates and deliveries. - `GET https://next-indexer.gibs.finance/version`: returns the indexer version, the schema name and the git commit. - Use lowercase hex in every filter. A checksummed address matches nothing. - Read `delivered` on a request. Do not read `delivery`: it can be `null` for a request that landed. - The package `@gibs/bridge-indexer` wraps these queries and lists the known defects. ## The quote service - `POST https://quotes.gibs.finance/v1/display-quotes` with body `{ "asks": [ ... ] }`, 1 to 24 asks. Each ask has `from {chainId, token}`, `to {chainId, token}` and `amount` (a decimal string in base units). - It answers `{ "answers": [ ... ] }` in ask order. Each provider slot is `quoted`, `refused` or `pending`. - Anyone can ask for display quotes, also a native app that sends no `Origin` header. Each client shares a limit of 60 requests a minute, with bursts of 20. - The forwarding routes carry the Gibs Finance provider keys. They accept only the Gibs Finance sites, and answer 403 to every other caller. - A display quote shows what to expect. It is never the transaction to sign. Build and check the real transaction separately, with `searchBridgeRoutes` and `planCandidateExecution`. - To run your own service, use `@gibs/quotes`. It carries the same wire contract. ## npm packages Each ships ECMAScript modules and CommonJS, runs in React Native 0.77 and later with no Metro setting, needs Node 20 or later, and takes viem `>=2.35.0 <3` as a peer dependency. - `@gibs/bridge-sdk`: prices and builds bridge transfers, and builds LI.FI, Relay and NEAR Intents requests. It never signs. - `@gibs/bridge-client`: browser-side RPC endpoints, cached viem clients, token metadata and chain logos. - `@gibs/bridge-indexer`: reads the indexer for transfers, deliveries and live status. It never signs. - `@gibs/abis`: the bridge contract ABIs as viem constants. - `@gibs/common`: shared helpers for clients, multicall, caching and bigint-safe JSON. - `@gibs/quotes`: a live quote client and route search across providers. ## Rules for integrators - PulseChain is the home side. Entering PulseChain and leaving it use different fees. Read the fee for your direction from the chain. - Bridge fees are a rate per 1e18. Aggregator referral fees are in basis points. - A token that was never bridged has no address on the far side yet. This is normal, not an error. - Use your own Relay API key and referrer, and your own LI.FI integrator name. The packages ship none. ## Links - [Releases repository](https://github.com/gibsfinance/interface-releases): signed releases, public keys and local copy downloads. ## Integration guide - [Integrate Gibs into a wallet or app](https://next.gibs.finance/docs/): search, plan, send, track and claim a transfer from your own wallet or app, with two complete examples. - [Everything in one file](https://next.gibs.finance/llms-full.txt): this index, the guide, the examples and every package guide, joined. - [example-a-ether-to-pulse.ts](https://next.gibs.finance/docs/examples/example-a-ether-to-pulse.ts.txt): an example file of the integration guide. - [example-b-token-pulsechain-to-ethereum.ts](https://next.gibs.finance/docs/examples/example-b-token-pulsechain-to-ethereum.ts.txt): an example file of the integration guide. - [shared.ts](https://next.gibs.finance/docs/examples/shared.ts.txt): an example file of the integration guide. - [tsconfig.json](https://next.gibs.finance/docs/examples/tsconfig.json): an example file of the integration guide. ## Package guides - [@gibs/abis](https://next.gibs.finance/docs/packages/abis/llms.txt): Contract interfaces for the Gibs Finance bridge, as viem-ready constants. - [@gibs/bridge-client](https://next.gibs.finance/docs/packages/bridge-client/llms.txt): Browser-side endpoints, clients, and caching for a Gibs Finance bridge interface. - [@gibs/bridge-indexer](https://next.gibs.finance/docs/packages/bridge-indexer/llms.txt): Reads a Gibs Finance bridge indexer: transfer archive, delivery receipts, and live crossing status. - [@gibs/bridge-sdk](https://next.gibs.finance/docs/packages/bridge-sdk/llms.txt): Build and price cross-chain transfers between PulseChain, Ethereum, and Binance Smart Chain. - [@gibs/common](https://next.gibs.finance/docs/packages/common/llms.txt): Low-level blockchain helpers shared across the Gibs Finance packages: caching, multicall, and serialization. - [@gibs/quotes](https://next.gibs.finance/docs/packages/quotes/llms.txt): Isomorphic core for asking cross-chain bridge aggregators (LI.FI, Relay, NEAR Intents) for display and execution quotes — the wire contract, refusal reading, and the shared guards a host wraps every upstream call in. Runs unchanged in a browser or in Node. --- # Integrate Gibs into a wallet or app This guide shows you how to add Gibs Finance bridge routes to your wallet, app or script. You search for routes, plan the transactions, send them with your own signer, track the transfer, and claim it when the bridge needs a claim. The guide covers the `@gibs/*` packages at version **1.16.0**. Check npm for the current version before you install. Never install 1.12.0 or older: those versions do not load. The packages never sign and never hold funds. Your wallet signs every transaction. A mobile wallet such as Internet Money uses the packages this way, and so can any other app. Terms used once and then kept: - **API** means application programming interface. **HTTP** means Hypertext Transfer Protocol. **JSON** means JavaScript Object Notation. - An **RPC endpoint** is a node's remote procedure call address, for example `https://your-node.example`. - A **section** is one wallet signature in a route. A route with a crossing and then a swap has two sections. - A **crossing** is one omnibridge transfer between PulseChain and Ethereum, or between PulseChain and BNB Smart Chain. - **Delivery** means that a paid service submits the last transaction of a crossing on the destination chain, so the user does not have to come back and claim. ## Contents 1. [What the packages do](#1-what-the-packages-do) 2. [Install and set up](#2-install-and-set-up) 3. [Search for routes](#3-search-for-routes) 4. [Plan the transactions](#4-plan-the-transactions) 5. [Track a transfer](#5-track-a-transfer) 6. [Claim a transfer](#6-claim-a-transfer) 7. [Defaults you replace](#7-defaults-you-replace) 8. [The public Gibs services](#8-the-public-gibs-services) 9. [Example A: ETH on Ethereum to PLS on PulseChain](#9-example-a-eth-on-ethereum-to-pls-on-pulsechain) 10. [Example B: an ERC-20 token from PulseChain to Ethereum, delivery off](#10-example-b-an-erc-20-token-from-pulsechain-to-ethereum-delivery-off) 11. [Fees and attribution](#11-fees-and-attribution) 12. [Known limits](#12-known-limits) The two examples are complete TypeScript files. They typecheck against the published packages: | File | What it holds | |---|---| | [`examples/shared.ts`](https://next.gibs.finance/docs/examples/shared.ts.txt) | The signer interface, a web3.js v4 adapter (in a comment), viem clients, the allowance and balance readers, the sender, the status poller | | [`examples/example-a-ether-to-pulse.ts`](https://next.gibs.finance/docs/examples/example-a-ether-to-pulse.ts.txt) | Example A | | [`examples/example-b-token-pulsechain-to-ethereum.ts`](https://next.gibs.finance/docs/examples/example-b-token-pulsechain-to-ethereum.ts.txt) | Example B | | [`examples/tsconfig.json`](https://next.gibs.finance/docs/examples/tsconfig.json) | Strict compiler settings with `moduleResolution: "Bundler"` | The files ship inside `@gibs/bridge-sdk`, in `node_modules/@gibs/bridge-sdk/docs/examples/`. To check them, install the packages and `typescript`, copy the folder into your project, and run `npx tsc -p examples/tsconfig.json`. The examples never ran against a live chain. --- ## 1. What the packages do | Step | Function | Package | |---|---|---| | Find and rank routes | `searchBridgeRoutes` | `@gibs/quotes/search` | | Get the live quote an aggregator section signs | `fetchLiveSectionQuote`, `fetchLiveQuote` | `@gibs/quotes/search` | | Turn a route into transactions | `planCandidateExecution` | `@gibs/bridge-sdk/execution` | | Read a fresh PulseX swap quote | `readPulseXSectionQuote` | `@gibs/bridge-sdk/execution` | | Read how a token maps across the bridge | `tokenBridgeInfo` | `@gibs/bridge-sdk/chain-info` | | Track a transfer | `fetchTransferStatus` | `@gibs/bridge-indexer` | | Read the record a claim needs | `fetchClaimRecords` | `@gibs/bridge-indexer` | | Build a claim | `buildClaimTransaction` | `@gibs/bridge-sdk/claims` | An aggregator is a third-party bridge service. The packages ask three of them: LI.FI, Relay and NEAR Intents. The omnibridge is the PulseChain bridge to Ethereum and to BNB Smart Chain. PulseX is the exchange on PulseChain. --- ## 2. Install and set up ### Packages ```sh npm install @gibs/quotes @gibs/bridge-sdk @gibs/bridge-indexer viem ``` `@gibs/quotes` pulls in `@gibs/bridge-sdk`, `@gibs/common` and `@gibs/abis`. `@gibs/bridge-indexer` pulls in `@gibs/bridge-client` and `graphql-request`. Every package needs Node 20 or later when it runs on Node. ### viem The peer range is `>=2.35.0 <3`. The packages are tested against exactly 2.35.0, the floor. Keep one copy of viem in your bundle. ### Module formats Every package ships two builds: ECMAScript modules in `dist/` and CommonJS in `dist/cjs/`. The `exports` map has `react-native`, `import` and `require` conditions. Any bundler that reads `exports` picks the right build. ### React Native 0.77 The packages work in React Native 0.77 and later with no Metro setting. They are tested in a React Native 0.77.3 app with viem 2.35.0 and the template's own `metro.config.js`, `babel.config.js` and `jest.config.js`. Every public entry point is bundled for iOS and Android, compiled with the Hermes compiler, and loaded under jest. Metro in React Native 0.77 ignores `exports`. So every package also ships one folder per subpath, such as `@gibs/bridge-sdk/execution/package.json`. Its `main` and `react-native` fields point at the CommonJS build. You import the subpath as usual. You do **not** need: - a `resolveRequest` hook in `metro.config.js`; - `transformIgnorePatterns` in your jest configuration. The React Native jest preset loads the CommonJS build untransformed; - `unstable_enablePackageExports`. Do not turn it on for these packages. They do not need it, and it can break other libraries in your app, such as web3.js. ### React Native's own URL is enough You need no URL polyfill. React Native 0.77's own `URL` supports only `href` and `toString()`. Its other getters throw "not implemented", and its `searchParams` is always empty. Its `URLSearchParams` can only append and serialize. No package reads either class. The packages build and read request addresses with `@gibs/common/url`, which needs only `encodeURIComponent` and `decodeURIComponent`. The tests load every public entry point under React Native's own `URL` and `URLSearchParams`. If your app installs a URL polyfill for other code, the packages still work. ### TON TON is The Open Network. Most wallets never load its code: - `@gibs/bridge-sdk/ton-connect` needs no TON library and no `Buffer`. - `@gibs/bridge-sdk/ton` needs `@ton/core`, a global `Buffer` and `expo-crypto`. Install `buffer` and set `globalThis.Buffer` before the import. Without `expo-crypto`, Metro cannot bundle it. - npm still installs `@ton/core` and `@ton/crypto`, because they are regular dependencies of `@gibs/bridge-sdk`. Metro bundles them only when you import `@gibs/bridge-sdk/ton`. A wallet that bridges only between Ethereum-style chains never pays for TON. ### Import narrow subpaths Metro does not drop unused exports. Import `@gibs/bridge-sdk/execution`, not `@gibs/bridge-sdk`. See [the bundle-size note](#bundle-size-with-exports-turned-on). --- ## 3. Search for routes `searchBridgeRoutes` in `@gibs/quotes/search` does the whole search in one call. It loads the provider lists, the omnibridge pairs, prices, gas prices and balances. Then it runs the route engine and decides which routes it may serve. `loadBridgeablePairs` in the same subpath lists the pairs the omnibridge carries. It takes: - `origin` and `destination`: the chain, the token, the amount, the sending account and the recipient; - `clientForChain`: your viem client for each chain, or `null` for a chain you have no endpoint for. The search skips what it cannot read; - `fetch`, an `AbortSignal`, and `timeouts`; - your keys and your attribution. See [Fees and attribution](#11-fees-and-attribution). A cancelled search rejects with `RouteSearchCancelledError`. **PulseChain never reaches an aggregator.** PulseChain is chain 369, and its testnet is chain 943. No request that names either chain goes to LI.FI, Relay or NEAR Intents. Three layers refuse it, the last one at the wire. **Your LI.FI key.** Pass `lifiApiKey`. It travels only as the `x-lifi-api-key` header, on every LI.FI request the search makes: the chain list, the token lists, the prices and every quote. It never enters a URL, a body or a log, and no other provider receives it. The search refuses any request that would carry it in a URL or a body. LI.FI advises against shipping a key in client code. If you keep the key on a server, point `lifiApiOrigin` at your own proxy, and let the proxy add the header. You can combine the two. Without either, LI.FI answers at its lower keyless rate limit. --- ## 4. Plan the transactions `planCandidateExecution` in `@gibs/bridge-sdk/execution` turns a served route into an ordered list of `PlannedTx`, one entry per transaction: ```ts type PlannedTx = { readonly chainId: number; readonly kind: 'approve' | 'bridge' | 'swap' | 'wrap' | 'unwrap' | 'claim'; readonly to: Hex; readonly data: Hex; readonly value: bigint; readonly gasLimit: bigint | null; // null: the wallet estimates readonly waitForReceipt: boolean; // true on every transaction except the last }; ``` What the plan guarantees: - **Approvals are exact.** The plan approves only the amount a transaction spends. It skips an approval that the current allowance covers. It resets a short, non-zero allowance to zero first. - **Spenders are checked.** The plan approves only these spenders: the omnibridge mediator and the PulseX router from the package configuration, the LI.FI Diamond (with the LI.FI execution guard), and the Relay depository (`relayDepositoryByChainId`). The plan refuses a quote that names any other spender or deposit target. - **Only live quotes are signed.** An aggregator section needs the live quote asked for your wallet, in `sections[i].aggregator`. The route's own display quote is never signed. Without a live quote, the plan refuses with `live-quote-required`. - **Deposits match the request amount.** The LI.FI guard checks the decoded swap amount. Relay must call `depositNative` or `depositErc20` with the requested token, value and amount. NEAR Intents must pay exactly the requested amount to the quote's own deposit address. The plan refuses a quote that disagrees with the request, with `amount-mismatch` or `recipient-mismatch`. - **Delivery is your explicit choice.** Each crossing takes `delivery: { shouldDeliver: false }` or `{ shouldDeliver: true, fee }`. There is no default. ### Plan one section at a time A route with two sections has a crossing, and then something on the far chain. The second section must spend what actually arrived, not what the search predicted. So plan each section when it is about to run: - Pass `firstSectionIndex`. Then `sections[0]` plans that section. Refusal indices and delivery indices stay absolute. - Plan section 1 after section 0's crossing arrived, with the amount that arrived. Every check above still applies to the later section. ### A fresh swap floor A PulseX section takes `swap.quotedAmountOut`, a fresh quote of its output. The slippage floor then comes from this quote, not from the search-time figure. `readPulseXSectionQuote({ hop, amountIn, client })` reads it along the exact path the swap signs, on the hop's own router. Pass a fresh `swap.deadline` too. ### The live quote `fetchLiveSectionQuote` in `@gibs/quotes/search` builds the request from the section itself, and asks the section's own provider. It returns `{ quote, request }`, which is exactly `sections[i].aggregator`. - It handles three shapes: a direct last section, a section that another section follows (it lands in your own wallet), and a section that fuses with an omnibridge entry. - For the fused shape, it reads the token mapping through your `clientForChain`. It shrinks the entry amount when the cost of the entry call would make the spend exceed `amountIn`. It asks at most three times. It never returns a quote that spends more than `amountIn`. - It takes your `fetch`, `keys` (`lifiApiKey`, `relayApiKey`, `nearIntentsApiKey`, each sent only as its own header), `attribution`, `lifiApiOrigin` and `signal`. `fetchLiveQuote` is the lower-level call: one provider, and one `ConsolidatedQuoteRequest` you built yourself. Both calls send through the same wire guard as the search. No request that names PulseChain, and no request that carries a key outside a header, ever leaves. --- ## 5. Track a transfer `fetchTransferStatus` in `@gibs/bridge-indexer` takes `{ chainId, txHash }` for the transaction that started a transfer. It returns: - `state`: one of `sent`, `confirmed-not-final`, `validators-confirming`, `ready-delivery-service`, `ready-your-turn`, `delivered`, `entry-reached`, `stranded`, `failed` and `unknown`. `transferStateNames` holds the words to show. - `claimable`: true in both ready states. - `destinationTxHash`: the transaction that settled the destination side. - `aggregatorStatus`: the aggregator's own answer, for a route that starts with an aggregator. Every option is optional: `fetch`, `endpoint`, `signal`, `aggregator`, `aggregatorFinalLeg` and `originClient`. The function works around every known indexer defect: it lowercases hex filters, and it avoids the one-way joins and the relations that are always null. What you pass: - `originClient`, your viem client on the origin chain. Without it, a transaction that the indexer has not recorded yet is `unknown`, not `sent`. - `aggregator`, for a route that starts with an aggregator. Relay's status endpoint answers 400 without your Relay key. --- ## 6. Claim a transfer A crossing out of PulseChain with delivery off ends in the `ready-your-turn` state. The user then claims the tokens on the destination chain, and pays the gas there. ### Read the record `fetchClaimRecords` in `@gibs/bridge-indexer` returns the bridge requests a claim is built from, oldest first, in the shape `buildClaimTransaction` takes: ```ts const records = await fetchClaimRecords({ source: { chainId: 369, txHash } }); // or: fetchClaimRecords({ messageHash }) ``` It asks for exactly the claim fields plus `messageHash`. It lowercases every hash, reads `delivered` as a flag, and never asks for a relation that the indexer leaves null. It makes no RPC read and writes no log. It takes `fetch`, `endpoint` and `signal`, like `fetchTransferStatus`. An empty list means that the indexer has not recorded the request yet. It is a separate call, not a field of `fetchTransferStatus`, for three reasons: - You poll the status often, and signatures and `encodedData` are large. - One transaction can start several requests. The status reports the slowest, but a claim is per request. - A claim can start from a message hash, as in a history list, with no status call at all. ### Build the claim `buildClaimTransaction` in `@gibs/bridge-sdk/claims` takes the record and a `via` choice: `{ kind: 'direct' }` or `{ kind: 'router', destinationRouter, validator }`. It returns `{ kind: 'claim', transaction }`, where the transaction is a `PlannedTx` of kind `claim` on the destination chain. Or it returns a typed refusal: | Refusal | Meaning | What your app does | |---|---|---| | `wrong-type` | The request is not a signature request. The validators execute it themselves. | Nothing to claim. | | `already-delivered` | The release already ran. | Show "delivered". | | `not-signed-yet` | The validators have not finished. | Wait, then ask again. | | `signatures-incomplete` | The indexer stored fewer signatures than the bridge needs. Carries `stored` and `needed`. | Stop. Waiting does not fix it. Report it to Gibs Finance. | | `malformed-request` | A field has the wrong shape. Carries `field`. | Stop. Report it. | See [the submitSignature case](#the-submitsignature-case) for the cause of `signatures-incomplete`. --- ## 7. Defaults you replace The packages send nothing on your behalf, and they use no private service unless you ask: - **RPC endpoints.** `chainsMetadata` carries only public endpoints. A shared demo key, `vk_demo`, lives only in `demoRpcUrls` and `demoRpcUrlsFor` (`@gibs/bridge-sdk/chains`). Every reader takes your own clients. Use your own nodes in production. - **No default LI.FI integrator.** No `integrator`, `referrer`, `referral` or fee is sent unless you pass one. - **No fee or referral unless you ask.** `@gibs/bridge-sdk/referral` holds Gibs Finance's own values. No builder reads them. Do not pass them: they pay Gibs Finance. - **NEAR Intents `User-Agent`.** The request builders (`nearIntentsRequestHeaders`) name Gibs Finance by default. `searchBridgeRoutes` sends none unless you pass `nearIntentsUserAgent`. Name your own app. - **Clients built for you.** `fetchBridgeTransactions` and `@gibs/bridge-client` still build their own clients on the demo endpoints. Do not use them in production. The examples do not. The `@gibs/bridge-sdk` README has a section, "Which fees apply, and how you control each one". --- ## 8. The public Gibs services ### The quote service `https://quotes.gibs.finance` answers display quotes for anyone, including a native app that sends no `Origin` header: - `POST https://quotes.gibs.finance/v1/display-quotes` with the body `{ "asks": [ ... ] }`, 1 to 24 asks. - It shares one limit per client (60 a minute, bursts of 20) and one provider budget with every other caller. A display quote shows what to expect. It is never the transaction to sign. The forwarding routes of the service carry Gibs Finance's provider keys and return calldata that you could sign. They accept only Gibs Finance's own sites, and answer 403 to your app. So execute routes through `searchBridgeRoutes` and `planCandidateExecution`, with your own LI.FI and Relay keys or with none. Use the quote service for display only, and do not depend on it for signing. To run your own quote service, use `@gibs/quotes`. ### The indexer `@gibs/bridge-indexer` reads `https://next-indexer.gibs.finance` by default. The Gibs Finance interface reads the same indexer, so you share its load and its upgrades. The production indexer at `https://indexer.gibs.finance` can lag the schema, and a query there can fail with an unknown field. - Set the endpoint once at startup with `setIndexerEndpoint(...)`, or pass `endpoint` to each `fetchTransferStatus` and `fetchClaimRecords` call. Both default to `indexerEndpoint()`, so one `setIndexerEndpoint` covers both. - The indexer has known data defects until a re-index runs. `fetchTransferStatus` and `fetchClaimRecords` work around them. If you query the indexer yourself, follow the rules in the `@gibs/bridge-indexer` README: lowercase every hex filter, read `delivered` and not `delivery`, and read `Completion` and `Delivery` by `messageHash`. ### The signed release list `https://ipfs.gibs.finance/version.json` lists every interface release, with its content id and channel. `https://ipfs.gibs.finance/version.json.sig` is a detached Ed25519 signature over the exact bytes of that file. The public keys are in [public-keys.json](https://github.com/gibsfinance/interface-releases/blob/HEAD/public-keys.json). To link your users to the Gibs Finance interface, read the latest release from this list, and check the signature over the raw bytes first. ### Token and chain images `https://gib.show` serves token and chain logos and token lists. It needs no key. `@gibs/bridge-sdk/image-links` builds its addresses. --- ## 9. Example A: ETH on Ethereum to PLS on PulseChain The full file is [`examples/example-a-ether-to-pulse.ts`](https://next.gibs.finance/docs/examples/example-a-ether-to-pulse.ts.txt). The helpers are in [`examples/shared.ts`](https://next.gibs.finance/docs/examples/shared.ts.txt). This section walks through the steps. ### The signer The packages do not depend on web3.js or on any wallet library. The examples expect this interface: ```ts export type WalletTransactionRequest = { readonly chainId: number; readonly from: Hex; readonly to: Hex; readonly data: Hex; readonly value: bigint; readonly gas?: bigint; // set only when the plan names a gas limit }; export type WalletSigner = { readonly address: Hex; /** Broadcasts on `request.chainId` and resolves with the hash, before mining. */ readonly sendTransaction: (request: WalletTransactionRequest) => Promise; }; ``` Here is a web3.js v4 adapter, with one `Web3` instance per chain. In web3.js v4, `web3.eth.sendTransaction` returns a PromiEvent. Awaiting it gives the mined receipt. The `transactionHash` event fires first, and this signer resolves with it: ```ts const signer: WalletSigner = { address: account, sendTransaction: (request) => new Promise((resolve, reject) => { const web3 = web3ByChainId.get(request.chainId); if (web3 === undefined) return reject(new Error(`no web3 for chain ${request.chainId}`)); web3.eth .sendTransaction({ from: request.from, to: request.to, data: request.data, value: request.value, ...(request.gas === undefined ? {} : { gas: request.gas }), }) .on('transactionHash', (hash) => resolve(hash as Hex)) .on('error', reject); }), }; ``` This adapter is not compiled with the examples, because they do not install web3.js. The examples wait for receipts through the viem clients, and only where the plan says `waitForReceipt`. ### Step 1: one viem client per chain, from your own RPC endpoints ```ts const clientForChain = createClientLookup( new Map([ [1, 'https://your-ethereum-node.example'], [369, 'https://your-pulsechain-node.example'], ]), ); const requireClient = requireClientWith(clientForChain); ``` `createClientLookup` calls viem's `createPublicClient` with `http(url)` for each chain. It answers `null` for a chain that you have no endpoint for. ### Step 2: search ```ts const search = await searchBridgeRoutes({ origin: { chainId: 1, token: { address: zeroAddress, decimals: 18, symbol: 'ETH' }, amount: amountWei, account: signer.address, }, destination: { chainId: 369, token: { address: zeroAddress, decimals: 18, symbol: 'PLS' }, recipient: signer.address, }, clientForChain, fetch: (input, init) => globalThis.fetch(input, init), signal, timeouts: { requestMs: 15_000, searchMs: 60_000 }, nearIntentsUserAgent: 'your-wallet/1.0', }); ``` The native coin is the zero address on both sides. ### Step 3: pick a route `search.routes` holds the routes the search may serve, one per provider, best first. The best is `search.routes[0]`. When the list is empty, `search.priceImpactRefusal` says whether price impact is the reason, and `search.refusals` lists every refusal. A served route has `candidate` (what you plan), `plan` (the engine's hops and signature groups), `route` (display data) and `routeSignatureCount`. For ETH to PLS, the usual best route is a crossing into PulseChain and then a PulseX swap: two sections. ### Step 4: build ONE section's inputs, from what it spends now The example plans and sends one section at a time. The first hop of a section names its carrier. The example builds one `SectionExecutionInputs` for it: - **Omnibridge** (`'pulsechain'` or `'tokensex'`): read the token mapping with `tokenBridgeInfo`, then state the delivery choice. When the crossing enters PulseChain, the bridge executes it itself. So `{ shouldDeliver: false }` is the only accepted choice, and the plan refuses `{ shouldDeliver: true, ... }` with `delivery-not-offered`. ```ts const assetLink = await tokenBridgeInfo({ bridgeKey: [carrier, toChain(from.chainId), toChain(to.chainId)], assetIn: search.resolveToken(from), isProd: true, fromChainClient: requireClient(from.chainId), toChainClient: requireClient(to.chainId), }); const inputs = { amountIn, recipient, omnibridge: { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false }, }; ``` - **PulseX**: read a fresh quote just before signing, for the amount that arrived. Then set a slippage floor below it, and a deadline that starts now. ```ts const quotedAmountOut = await readPulseXSectionQuote({ hop: firstHop, amountIn, client: requireClient(369) }); const inputs = { amountIn, recipient, swap: { slippageBasisPoints: 100n, deadline: deadlineIn(20 * 60), quotedAmountOut } }; ``` - **Wrap**: `{ amountIn, recipient }` only. - **Any aggregator**: the live quote, asked now for this wallet, this recipient and this amount. ```ts const live = await fetchLiveSectionQuote({ plan: served.plan, sectionIndex, resolveToken: search.resolveToken, amountIn, account: signer.address, recipient, clientForChain, // reads the token mapping of a section that fuses with an omnibridge entry fetch: (input, init) => globalThis.fetch(input, init), keys: { lifiApiKey, relayApiKey }, signal, }); if (live.kind !== 'quoted') throw new Error(`no live quote (${live.kind})`); const inputs = { amountIn, recipient, aggregator: { quote: live.quote, request: live.request } }; ``` The first section spends `amountWei`. Each later section spends what actually arrived from the section before it. The example reads the next section's token balance on its chain before the section runs, and again after the crossing is delivered. The difference is what arrived. ### Step 5: plan that section alone ```ts const plan = await planCandidateExecution({ candidate: served.candidate, account: signer.address, readAllowance: allowanceReaderWith(requireClient), // (chainId, token, owner, spender) => Promise resolveToken: search.resolveToken, firstSectionIndex: sectionIndex, sections: [inputs], }); if (plan.kind === 'refused') { // plan.sectionIndex (absolute) and plan.refusal.reason say which section and why. } ``` Show `plan.deliveries` and the fees before the user signs. For the usual route, the two sections plan to: | Section | Order | Chain | Kind | Waits for receipt | |---|---|---|---|---| | 0 | 1 | Ethereum | `bridge` (`wrapAndRelayTokens`, value = the ether) | no (last of its plan) | | 1 | 1 | PulseChain | `approve` (wrapped ether from Ethereum, to the PulseX router, exact amount) | yes | | 1 | 2 | PulseChain | `swap` | no | Native ether needs no approval. ### Step 6: send, then wait for arrival before the next section `sendPlannedTransactions` (in `shared.ts`) sends each transaction of the section in order. It broadcasts through your signer, waits for the receipt where `waitForReceipt` is true, and throws on a revert. The example then waits for the last receipt of the section. A receipt is not arrival. It only says that the crossing started. So before the example plans the next section, it polls `fetchTransferStatus` on the crossing transaction until the state is `delivered`. Then it measures what arrived. ### Step 7: poll until delivered ```ts const status = await waitForTransferStatus({ source: { chainId: 1, txHash: crossingHash }, statusOptions: { signal, originClient: requireClient(1) }, isDone: (answer) => answer.state === 'delivered', intervalMs: 15_000, }); ``` `waitForTransferStatus` throws `TransferEndedError` on `failed` or `stranded`. If the route started with an aggregator, the example adds `aggregator` to the status options: `{ provider: 'lifi' }`, `{ provider: 'relay', requestId: quote.quoteId, apiKey }` or `{ provider: 'near-intents', depositAddress: quote.quoteId }`. A route that ends with a swap on PulseChain is done when the receipt of that swap succeeds. --- ## 10. Example B: an ERC-20 token from PulseChain to Ethereum, delivery off The full file is [`examples/example-b-token-pulsechain-to-ethereum.ts`](https://next.gibs.finance/docs/examples/example-b-token-pulsechain-to-ethereum.ts.txt). The token is PulseX (PLSX), which is native to PulseChain. ### Step 1: map the token and search ```ts const assetLink = await tokenBridgeInfo({ bridgeKey: [Providers.PULSECHAIN, Chains.PLS, Chains.ETH], assetIn: plsx, isProd: true, fromChainClient: pulsechainClient, toChainClient: ethereumClient, }); ``` `assetLink.assetOutAddress` is the token's address on Ethereum. A `null` value is a normal answer: the token has not crossed before, and the first transfer creates it. The example stops there, because the search needs a destination address. Then the example calls `searchBridgeRoutes` with PulseChain as the origin and Ethereum as the destination. It picks the served route that has one section whose hop is the omnibridge (`plan.hops[0].edge.carrier === Providers.PULSECHAIN`). ### Step 2: plan with delivery off ```ts const plan = await planCandidateExecution({ candidate: crossing.candidate, account: signer.address, readAllowance: allowanceReaderWith(requireClient), resolveToken: search.resolveToken, sections: [ { amountIn: amount, recipient: signer.address, omnibridge: { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false }, }, ], }); ``` The plan is `approve` and then `bridge` (`relayTokens`), both on PulseChain. The approval is for the exact amount, to the PulseChain mediator. The plan skips it when the allowance covers the amount. `plan.deliveries` is `[{ legIndex: 0, status: 'not-requested' }]`. With delivery off, nobody is paid to release the tokens on Ethereum. The user claims them there and pays the Ethereum gas. Leaving PulseChain over Ethereum also costs the bridge fee. Read it with `loadBridgeFees`. ### Step 3: poll until claimable ```ts const ready = await waitForTransferStatus({ source: { chainId: 369, txHash: bridgeHash }, statusOptions: { signal, originClient: pulsechainClient }, isDone: (status) => status.claimable || status.state === 'delivered', intervalMs: 30_000, }); ``` With delivery off, the ready state is `ready-your-turn`. If the state is already `delivered`, somebody else released the tokens, and there is nothing to claim. ### Step 4: build the claim, and handle every refusal The example reads the record with `fetchClaimRecords`. The function lowercases the hash for you, and asks for exactly the fields a claim reads: ```ts const records = await fetchClaimRecords({ source: { chainId: 369, txHash: bridgeHash }, signal }); const [request] = records; // a plain relayTokens starts exactly one request const claim = buildClaimTransaction({ request, via: { kind: 'direct' } }); ``` The example stops with an error unless the transaction started exactly one request. `via: { kind: 'direct' }` sends the claim straight to the Ethereum bridge and pays no tip. That is correct with delivery off. The `router` choice is for a request that carries a delivery fee. The example maps each refusal to an action in an exhaustive `switch`. A new refusal reason in a later version fails the typecheck: - `already-delivered`: done. Poll until `delivered` to get `destinationTxHash`. - `not-signed-yet`: wait 30 seconds and read the record again. This can happen when the status and the record are read a moment apart. - `signatures-incomplete`, `wrong-type`, `malformed-request`: stop with a `ClaimRefusedError`. Do not retry. ### Step 5: send the claim on Ethereum `claim.transaction.chainId` is 1. The example sends it through the same signer, waits for the receipt, and polls `fetchTransferStatus` until `delivered`. `destinationTxHash` is then the transaction of the claim. --- ## 11. Fees and attribution **Nothing is added for you.** No builder, search or plan adds a fee, a payout address or a Gibs Finance label unless you pass it. Every fee below is an on-chain setting you read, a charge of a provider that you show, or a fee that you chose. | Fee | When it applies | How your app sees it | How your app controls it | |---|---|---|---| | Bridge fee | Every omnibridge crossing. It is an on-chain rate for each direction. At the time of writing: 0% entering PulseChain over Ethereum, 0.3% leaving it; 2.97% each way over BNB Smart Chain. | `candidate.omnibridgeFeeAmount` on the route. `loadBridgeFees` (`@gibs/bridge-sdk/chain-info`) for the rate, then `bridgeCost`. | It cannot be turned off. Show it. | | Delivery fee | Only when a crossing out of PulseChain has `shouldDeliver: true`. | `plan.deliveries[i].maximumFee` and `feeToken`. `deliveryFee` in `@gibs/bridge-sdk/settings`. | Choose `{ shouldDeliver: false }` (the user claims and pays gas), or set the fee terms yourself in `QuotedDeliveryFee`. | | Provider fees (gas, solver, protocol) | Every aggregator section. | The `fees` of the live quote (`QuoteFee[]`). A fee with `includedInFromAmount: false` is paid **in addition to** `fromAmount`. | Not yours to control. Show them. | | NEAR Intents keyless surcharge | Every NEAR Intents quote without a partner key: 25 basis points. | The NEAR Intents parser reports it as a charge. | Get a partner key and pass `nearIntentsApiKey`. | | PulseX swap | Every PulseX section. The pool fee and the price impact are inside the quoted output. | The quoted output of the hop, and `minAmountOut` in the swap calldata. | `swap.slippageBasisPoints` sets the floor. | | Aggregator cushion | A route that enters the omnibridge through an aggregator asks for an exact output. The amount that the aggregator does not need stays in the wallet on the entry chain. | Compare the `fromAmount` of the live quote with what arrives. | By design. Tell the user that it is theirs. | | Your LI.FI fee | Only when you pass `lifiFee` and your own `lifiIntegrator`. | The `fee` field of the LI.FI request. | Leave it out to charge nothing. A positive fee without your integrator throws. | | Your Relay fee | Only when you pass `relayAppFee: { recipient, basisPoints }`. | The `appFees` field of the Relay request. | Leave it out to charge nothing. | | Gas | Every transaction. | `search.gasCostFor` and `search.isGasless` on the search result. | Your wallet's gas settings. `gasLimit: null` means that you estimate. | Bridge fees are a rate per 1e18. Aggregator referral fees are in basis points. **Your attribution.** Pass it in one place for the search, and in the same fields of the `ConsolidatedQuoteRequest` for the live quote: ```ts await searchBridgeRoutes({ // ... attribution: { lifiIntegrator: 'your-wallet', lifiFee: 0.001, // optional: a tenth of a percent, paid to your LI.FI integrator account relayReferrer: 'your-wallet', // Relay then requires your key relayAppFee: { recipient: '0xYourFeeWallet', basisPoints: 10n }, // optional }, relayApiKey: yourRelayKey, nearIntentsApiKey: yourNearKey, nearIntentsUserAgent: 'your-wallet/1.0', }); ``` Each attribution field is either left out (nothing is sent) or your own string. Do not pass anything from `@gibs/bridge-sdk/referral`. Those values pay Gibs Finance. --- ## 12. Known limits ### The submitSignature case The indexer stores a validator's signature only when the validator calls `submitSignature` directly on the home bridge. The indexer decodes the input of the transaction, not the event. A validator that signs through a multisig wallet, a proxy or a batching contract still emits `SignedForUserRequest`. The indexer counts that signature in `confirmedSignatures`, but it does not store it in `signatures`. `CollectedSignatures` still sets `finishedSigning` to true. So the record says "signed" with too few signatures, and a claim built from it reverts. `buildClaimTransaction` catches this case and refuses with `signatures-incomplete`, with `stored` and `needed`. Waiting does not fix it, because the indexer never goes back for the missing signature. Tell the user that the transfer is safe but that your app cannot claim it yet, and report it to Gibs Finance. The indexer defect is not fixed in 1.16.0. ### Relay deposits must use `depositNative` or `depositErc20` The plan accepts a Relay deposit only when it calls `depositNative` or `depositErc20` on the depository from `relayDepositoryByChainId`, with exactly the requested token, value and amount. The check is strict on purpose. Relay can change its deposit form, add a chain, or move a depository. Until a new package version updates the table, the plan then refuses those Relay routes with `deposit-shape` or `deposit-target-not-allowed`. The table was read from Relay's chains API on 2026-10-07. A refused section does not lose funds. Fall back to the next route in `search.routes`. ### Backward-solved routes A route that enters the omnibridge through an aggregator is solved backwards. The omnibridge entry call needs an exact amount, so the provider is asked for an exact *output*, and it works out the input. The request then names no input of yours. So `aggregatorOriginCalls` cannot compare the deposit with a figure that the user chose. It can only check that the quote, the approval and the deposit agree with each other. Before the user signs, show the `fromAmount` of the live quote, and compare it with what the user expects. Refuse it in your own code if it is outside your tolerance. ### Bundle size with exports turned on React Native 0.77's Metro ignores `exports`, and the packages load through the subpath folders and the CommonJS build. With `exports` turned on (the default from React Native 0.79), a bundle of every wallet entry point grew from 1.75 MB to 2.69 MB in our measurements on version 1.15.0. The full cause is not traced. With `exports` on, Metro gives `import` statements the ECMAScript module build and `require` calls the CommonJS build. Code that is reached both ways can then be bundled twice. Measure your own bundle before you turn exports on. ### A token that never crossed A token that was never bridged has no address on the far side yet. `tokenBridgeInfo` then returns `assetOutAddress: null`. This is normal, not an error. The first transfer creates the token on the far side. --- # Example file: example-a-ether-to-pulse.ts Source: https://next.gibs.finance/docs/examples/example-a-ether-to-pulse.ts.txt ```ts /** * Example A: ETH on Ethereum to PLS on PulseChain. * * 1. Build viem clients from the wallet's own RPC endpoints. * 2. Search with `searchBridgeRoutes` and pick the best served route. * 3. Plan ONE section at a time with `planCandidateExecution` * (`firstSectionIndex`), so a later section spends what actually arrived, * with a fresh swap floor and deadline, or a fresh aggregator quote. * 4. Send each section's transactions, honouring `waitForReceipt`, and wait * for the crossing to arrive before planning the next section. * 5. Poll `fetchTransferStatus` until the last crossing is delivered. * * Typechecked only (`docs/examples/tsconfig.json` in `@gibs/bridge-sdk`). Never run against mainnet. */ import type { AggregatorStatusSource, FetchTransferStatusOptions } from '@gibs/bridge-indexer'; import { tokenBridgeInfo } from '@gibs/bridge-sdk/chain-info'; import { type Provider, Providers, toChain } from '@gibs/bridge-sdk/config'; import type { ConsolidatedQuote } from '@gibs/bridge-sdk/consolidated-quote'; import { type CandidateExecutionPlan, planCandidateExecution, readPulseXSectionQuote, type SectionExecutionInputs, } from '@gibs/bridge-sdk/execution'; import type { PlannedHop, ResolveNodeToken, RoutePlan } from '@gibs/bridge-sdk/routing'; import { fetchLiveSectionQuote, type ServedBridgeRoute, searchBridgeRoutes } from '@gibs/quotes/search'; import { type Hex, type PublicClient, zeroAddress } from 'viem'; import { allowanceReaderWith, createClientLookup, deadlineIn, type RpcUrlByChainId, readBalance, requireClientWith, type SentTransaction, sendPlannedTransactions, type WalletSigner, waitForTransferStatus, } from './shared.js'; const ethereumChainId = 1; const pulsechainChainId = 369; /** What the example needs from the wallet. */ export type EtherToPulseOptions = { readonly signer: WalletSigner; readonly rpcUrlByChainId: RpcUrlByChainId; /** The ether to send, in wei. */ readonly amountWei: bigint; /** Cancels the search, every live quote and every status poll. */ readonly signal: AbortSignal; /** The wallet's own LI.FI key, sent only as the `x-lifi-api-key` header. Leave it out to use LI.FI's keyless tier. */ readonly lifiApiKey?: string; /** The wallet's own Relay key, sent only as a header. Leave it out to use Relay's anonymous tier. */ readonly relayApiKey?: string; }; const omnibridgeCarriers: ReadonlySet = new Set(Object.values(Providers)); const isOmnibridgeCarrier = (carrier: string): carrier is Provider => omnibridgeCarriers.has(carrier); /** A plan's sections, one per signature, each as its hops. */ const sectionsOf = (plan: RoutePlan): readonly (readonly PlannedHop[])[] => plan.signatures.map((group) => group.flatMap((hopIndex) => plan.hops[hopIndex] ?? [])); /** Where `fetchTransferStatus` asks about an aggregator's leg. */ const aggregatorSourceOf = ( quote: ConsolidatedQuote, relayApiKey: string | undefined, ): AggregatorStatusSource => { if (quote.provider === 'lifi') return { provider: 'lifi' }; if (quote.provider === 'relay') { return { provider: 'relay', requestId: quote.quoteId, ...(relayApiKey === undefined ? {} : { apiKey: relayApiKey }), }; } return { provider: 'near-intents', depositAddress: quote.quoteId }; }; /** One section's inputs, and the live quote it signs when an aggregator carries it. */ type SectionPlanInputs = { readonly inputs: SectionExecutionInputs; readonly liveQuote: ConsolidatedQuote | null; }; /** * Builds the inputs for ONE section, from what it spends NOW: the typed * amount for the first section, what actually arrived for a later one. */ const buildSectionInputs = async (options: { readonly served: ServedBridgeRoute; readonly sectionIndex: number; readonly hops: readonly PlannedHop[]; readonly amountIn: bigint; readonly resolveToken: ResolveNodeToken; readonly account: Hex; readonly requireClient: (chainId: number) => PublicClient; readonly clientForChain: (chainId: number) => PublicClient | null; readonly signal: AbortSignal; readonly lifiApiKey: string | undefined; readonly relayApiKey: string | undefined; }): Promise => { const { served, sectionIndex, hops, amountIn, resolveToken, account, requireClient } = options; const firstHop = hops[0]; if (firstHop === undefined) throw new Error(`section ${sectionIndex} has no hops`); // Every section delivers to the signer; this example sends to the signer too. const recipient = account; const { carrier, from, to } = firstHop.edge; if (isOmnibridgeCarrier(carrier)) { // How the token maps across the bridge. Native ether reads the wrapped-ether mapping. const assetLink = await tokenBridgeInfo({ bridgeKey: [carrier, toChain(from.chainId), toChain(to.chainId)], assetIn: resolveToken(from), isProd: true, fromChainClient: requireClient(from.chainId), toChainClient: requireClient(to.chainId), }); if (assetLink === null) throw new Error(`no omnibridge mapping for ${from.address} on chain ${from.chainId}`); // The explicit delivery choice. Entering PulseChain, the bridge itself // executes the crossing, so `shouldDeliver: false` is the only accepted // choice here. Leaving PulseChain, see Example B. const omnibridge = { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false } as const; return { inputs: { amountIn, recipient, omnibridge }, liveQuote: null }; } if (carrier === 'PulseX') { // A FRESH QUOTE, read now along the exact path the swap signs, for the // amount that actually arrived. The floor is one percent below it, and the // deadline starts now, not before the crossing. const quotedAmountOut = await readPulseXSectionQuote({ hop: firstHop, amountIn, client: requireClient(from.chainId), }); if (quotedAmountOut === null) throw new Error(`section ${sectionIndex}: PulseX gave no quote`); return { inputs: { amountIn, recipient, swap: { slippageBasisPoints: 100n, deadline: deadlineIn(20 * 60), quotedAmountOut } }, liveQuote: null, }; } if (carrier === 'wrap') return { inputs: { amountIn, recipient }, liveQuote: null }; // Any other carrier is an aggregator. It signs only a LIVE quote, asked now // for this wallet, this recipient and this amount. const live = await fetchLiveSectionQuote({ plan: served.plan, sectionIndex, resolveToken, amountIn, account, recipient, clientForChain: options.clientForChain, fetch: (input, init) => globalThis.fetch(input, init), signal: options.signal, keys: { ...(options.lifiApiKey === undefined ? {} : { lifiApiKey: options.lifiApiKey }), ...(options.relayApiKey === undefined ? {} : { relayApiKey: options.relayApiKey }), }, nearIntentsUserAgent: 'your-wallet/1.0', }); if (live.kind !== 'quoted') throw new Error(`section ${sectionIndex}: no live quote (${live.kind})`); return { inputs: { amountIn, recipient, aggregator: { quote: live.quote, request: live.request } }, liveQuote: live.quote }; }; /** Turns a refused plan into an error a person can act on. */ const assertPlanned = ( plan: CandidateExecutionPlan, ): Extract => { if (plan.kind === 'planned') return plan; throw new Error( `section ${plan.sectionIndex} cannot be planned: ${JSON.stringify(plan.refusal, (_key, value) => (typeof value === 'bigint' ? value.toString() : value))}`, ); }; /** * Sends ether from Ethereum and delivers Pulse on PulseChain. * * @param options - see {@link EtherToPulseOptions} * @returns the transactions sent, and the final status of the last crossing */ export const sendEtherToPulse = async (options: EtherToPulseOptions) => { const { signer, signal } = options; const clientForChain = createClientLookup(options.rpcUrlByChainId); const requireClient = requireClientWith(clientForChain); // 1. Search. No attribution and no fee: the wallet names none here. const search = await searchBridgeRoutes({ origin: { chainId: ethereumChainId, token: { address: zeroAddress, decimals: 18, symbol: 'ETH' }, amount: options.amountWei, account: signer.address, }, destination: { chainId: pulsechainChainId, token: { address: zeroAddress, decimals: 18, symbol: 'PLS' }, recipient: signer.address, }, clientForChain, fetch: (input, init) => globalThis.fetch(input, init), signal, timeouts: { requestMs: 15_000, searchMs: 60_000 }, ...(options.lifiApiKey === undefined ? {} : { lifiApiKey: options.lifiApiKey }), ...(options.relayApiKey === undefined ? {} : { relayApiKey: options.relayApiKey }), nearIntentsUserAgent: 'your-wallet/1.0', }); // 2. Pick a route. `routes` is best first, one per provider. const served = search.routes[0]; if (served === undefined) { const why = search.priceImpactRefusal === null ? `${search.refusals.length} refusals` : 'price impact'; throw new Error(`no route can be served (${why})`); } const hopsBySection = sectionsOf(served.plan); const statusOptionsFor = ( crossing: SentTransaction, liveQuote: ConsolidatedQuote | null, ): FetchTransferStatusOptions => ({ signal, originClient: requireClient(crossing.transaction.chainId), ...(liveQuote === null ? {} : { aggregator: aggregatorSourceOf(liveQuote, options.relayApiKey) }), }); const waitForDelivered = (crossing: SentTransaction, liveQuote: ConsolidatedQuote | null) => waitForTransferStatus({ source: { chainId: crossing.transaction.chainId, txHash: crossing.hash }, statusOptions: statusOptionsFor(crossing, liveQuote), isDone: (status) => status.state === 'delivered', intervalMs: 15_000, }); // 3 and 4. One section at a time. The first spends the typed amount; each // later one spends what actually arrived from the one before it. const sent: SentTransaction[] = []; let amountIn = options.amountWei; for (const [sectionIndex, hops] of hopsBySection.entries()) { const { inputs, liveQuote } = await buildSectionInputs({ served, sectionIndex, hops, amountIn, resolveToken: search.resolveToken, account: signer.address, requireClient, clientForChain, signal, lifiApiKey: options.lifiApiKey, relayApiKey: options.relayApiKey, }); const plan = assertPlanned( await planCandidateExecution({ candidate: served.candidate, account: signer.address, readAllowance: allowanceReaderWith(requireClient), resolveToken: search.resolveToken, firstSectionIndex: sectionIndex, sections: [inputs], }), ); // Show `plan.deliveries` and every fee before the user signs. See "Fees and attribution". // What the next section will spend arrives on its own chain. Read the // balance there before this section runs, and again once it arrived. const nextHop = hopsBySection[sectionIndex + 1]?.[0]; const nextToken = nextHop === undefined ? null : search.resolveToken(nextHop.edge.from); const readNextBalance = () => nextToken === null ? Promise.resolve(0n) : readBalance({ client: requireClient(nextToken.chainId), token: nextToken.address, owner: signer.address }); const balanceBefore = await readNextBalance(); // Every transaction of one section is on one chain. const sectionSent = await sendPlannedTransactions({ transactions: plan.transactions, signer, requireClient, waitForArrival: async () => {}, }); sent.push(...sectionSent); const last = sectionSent.at(-1); if (last === undefined) throw new Error(`section ${sectionIndex} planned no transactions`); const receipt = await requireClient(last.transaction.chainId).waitForTransactionReceipt({ hash: last.hash }); if (receipt.status !== 'success') throw new Error(`the transaction ${last.hash} reverted`); const crossesChains = last.transaction.kind === 'bridge'; // 5. The route ends with this section. A crossing is done when delivered; // a swap is done when its receipt succeeds. if (nextToken === null) { return { sent, finalStatus: crossesChains ? await waitForDelivered(last, liveQuote) : null }; } if (crossesChains) await waitForDelivered(last, liveQuote); amountIn = (await readNextBalance()) - balanceBefore; if (amountIn <= 0n) throw new Error(`nothing arrived for section ${sectionIndex + 1}`); } throw new Error('the route had no sections'); }; ``` --- # Example file: example-b-token-pulsechain-to-ethereum.ts Source: https://next.gibs.finance/docs/examples/example-b-token-pulsechain-to-ethereum.ts.txt ```ts /** * Example B: an ERC-20 token from PulseChain to Ethereum, with delivery off. * * 1. Read how the token maps across the bridge, and search for the route. * 2. Plan the direct crossing with `shouldDeliver: false`: an exact approval, * then `relayTokens`. * 3. Poll `fetchTransferStatus` until the crossing is `claimable`. * 4. Read the bridge request with `fetchClaimRecords` and build the claim * with `buildClaimTransaction`, handling every typed refusal. * 5. Send the claim on Ethereum, then poll until the transfer is delivered. * * Typechecked only (`docs/examples/tsconfig.json` in `@gibs/bridge-sdk`). Never run against mainnet. */ import { type ClaimRecord, fetchClaimRecords, type TransferStatus } from '@gibs/bridge-indexer'; import { tokenBridgeInfo } from '@gibs/bridge-sdk/chain-info'; import { buildClaimTransaction, type ClaimRefusal } from '@gibs/bridge-sdk/claims'; import { Chains, Providers } from '@gibs/bridge-sdk/config'; import { planCandidateExecution } from '@gibs/bridge-sdk/execution'; import type { BridgeKey, Token } from '@gibs/bridge-sdk/types'; import { searchBridgeRoutes } from '@gibs/quotes/search'; import type { Hex } from 'viem'; import { allowanceReaderWith, createClientLookup, type RpcUrlByChainId, requireClientWith, sendPlannedTransactions, sleep, type WalletSigner, waitForTransferStatus, } from './shared.js'; const pulsechainChainId = 369; /** PulseX (PLSX), an ERC-20 token native to PulseChain. */ const plsx: Token = { chainId: pulsechainChainId, address: '0x95B303987A60C71504D99Aa1b13B4DA07b0790ab', decimals: 18, symbol: 'PLSX', name: 'PulseX', logoURI: null, }; const pulsechainToEthereum: BridgeKey = [Providers.PULSECHAIN, Chains.PLS, Chains.ETH]; /** What the example needs from the wallet. */ export type TokenToEthereumOptions = { readonly signer: WalletSigner; readonly rpcUrlByChainId: RpcUrlByChainId; /** The amount to send, in the token's base units. */ readonly amount: bigint; /** Cancels the search and every poll. */ readonly signal: AbortSignal; }; /** A claim the wallet must not send, and must not retry by waiting. */ export class ClaimRefusedError extends Error { constructor(readonly refusal: ClaimRefusal) { super(`the claim was refused: ${refusal.reason}`); this.name = 'ClaimRefusedError'; } } /** * Reads the one bridge request a crossing transaction started. * `fetchClaimRecords` asks the indexer for exactly the fields a claim reads, * lowercases the hash (the indexer matches hex in lowercase only), and never * reads a relation the indexer leaves null. * * @param options.txHash - the crossing transaction on PulseChain * @param options.signal - cancels the request * @returns the record, in the shape `buildClaimTransaction` takes * @throws on a network or GraphQL failure, or when the transaction started other than one request */ const readClaimRecord = async (options: { readonly txHash: Hex; readonly signal: AbortSignal; }): Promise => { const records = await fetchClaimRecords({ source: { chainId: pulsechainChainId, txHash: options.txHash }, signal: options.signal, }); const [record] = records; if (record === undefined || records.length !== 1) { throw new Error(`expected one bridge request for ${options.txHash}, found ${records.length}`); } return record; }; /** What to do with a refused claim. */ type RefusalAction = | { readonly kind: 'done' } | { readonly kind: 'wait' } | { readonly kind: 'stop'; readonly error: Error }; /** * Decides what each typed refusal means for the wallet. The switch is * exhaustive: a new refusal reason fails the typecheck here. * * @param refusal - the refusal from `buildClaimTransaction` * @returns the action */ const actionForRefusal = (refusal: ClaimRefusal): RefusalAction => { switch (refusal.reason) { case 'already-delivered': // Somebody executed the release already. Nothing to send. return { kind: 'done' }; case 'not-signed-yet': // The validators are still signing. Ask again later. return { kind: 'wait' }; case 'signatures-incomplete': // The indexer stored fewer signatures than the bridge needs (see // "Known limits": the submitSignature case). Waiting does not fix it. // Tell the user, and report the transfer to Gibs. return { kind: 'stop', error: new ClaimRefusedError(refusal) }; case 'wrong-type': // Not a signature request. The validators execute an affirmation themselves. return { kind: 'stop', error: new ClaimRefusedError(refusal) }; case 'malformed-request': // A field is not the shape the indexer documents. Report it. return { kind: 'stop', error: new ClaimRefusedError(refusal) }; default: { const unhandled: never = refusal; return { kind: 'stop', error: new Error(`unhandled refusal ${JSON.stringify(unhandled)}`) }; } } }; /** * Sends an ERC-20 token from PulseChain to Ethereum and claims it there by hand. * * @param options - see {@link TokenToEthereumOptions} * @returns the crossing hash, the claim hash (null when somebody else released it), and the final status */ export const sendTokenToEthereum = async ( options: TokenToEthereumOptions, ): Promise<{ readonly crossingHash: Hex; readonly claimHash: Hex | null; readonly finalStatus: TransferStatus; }> => { const { signer, signal } = options; const clientForChain = createClientLookup(options.rpcUrlByChainId); const requireClient = requireClientWith(clientForChain); const pulsechainClient = requireClient(pulsechainChainId); const ethereumClient = requireClient(Number(Chains.ETH)); // 1. How PLSX maps onto Ethereum. A null `assetOutAddress` means the token // has not crossed before; the first transfer creates it. The search needs // a destination address, so this example stops there. const assetLink = await tokenBridgeInfo({ bridgeKey: pulsechainToEthereum, assetIn: plsx, isProd: true, fromChainClient: pulsechainClient, toChainClient: ethereumClient, }); if (assetLink === null || assetLink.assetOutAddress === null) { throw new Error('PLSX has no bridged address on Ethereum yet'); } const search = await searchBridgeRoutes({ origin: { chainId: pulsechainChainId, token: plsx, amount: options.amount, account: signer.address, }, destination: { chainId: Number(Chains.ETH), token: { address: assetLink.assetOutAddress, decimals: plsx.decimals, symbol: plsx.symbol }, recipient: signer.address, }, clientForChain, fetch: (input, init) => globalThis.fetch(input, init), signal, }); // The direct crossing: one section, one omnibridge hop. const crossing = search.routes.find( ({ plan }) => plan.signatures.length === 1 && plan.hops[0]?.edge.carrier === Providers.PULSECHAIN, ); if (crossing === undefined) throw new Error('the search served no direct crossing for this token'); // 2. Plan it. Delivery is off: nobody is paid to release it on Ethereum, so // the user claims it there and pays the Ethereum gas. const plan = await planCandidateExecution({ candidate: crossing.candidate, account: signer.address, readAllowance: allowanceReaderWith(requireClient), resolveToken: search.resolveToken, sections: [ { amountIn: options.amount, recipient: signer.address, omnibridge: { isProd: true, assetLink, delivery: { shouldDeliver: false }, unwrap: false }, }, ], }); if (plan.kind === 'refused') throw new Error(`cannot plan the crossing: ${plan.refusal.reason}`); // plan.transactions is [approve (when the allowance is short), bridge], both on PulseChain. const sent = await sendPlannedTransactions({ transactions: plan.transactions, signer, requireClient, // Every transaction is on PulseChain, so this is never called. waitForArrival: async () => {}, }); const bridge = sent.find(({ transaction }) => transaction.kind === 'bridge'); if (bridge === undefined) throw new Error('the plan held no bridge transaction'); const source = { chainId: pulsechainChainId, txHash: bridge.hash } as const; const statusOptions = { signal, originClient: pulsechainClient } as const; // 3. Wait until the validators have signed. `claimable` is true in both // ready states. `delivered` means somebody released it already. const ready = await waitForTransferStatus({ source, statusOptions, isDone: (status) => status.claimable || status.state === 'delivered', intervalMs: 30_000, }); if (ready.state === 'delivered') return { crossingHash: bridge.hash, claimHash: null, finalStatus: ready }; // 4. Build the claim from the indexer's record. Retry only on `not-signed-yet`. for (;;) { const request = await readClaimRecord({ txHash: bridge.hash, signal }); // Delivery was off, so the claim goes straight to the destination bridge and pays no tip. const claim = buildClaimTransaction({ request, via: { kind: 'direct' } }); if (claim.kind === 'refused') { const action = actionForRefusal(claim.refusal); if (action.kind === 'stop') throw action.error; if (action.kind === 'done') { const finalStatus = await waitForTransferStatus({ source, statusOptions, isDone: (status) => status.state === 'delivered', intervalMs: 15_000, }); return { crossingHash: bridge.hash, claimHash: null, finalStatus }; } await sleep(30_000, signal); continue; } // 5. Send the claim on Ethereum (`claim.transaction.chainId` is 1). const [claimSent] = await sendPlannedTransactions({ transactions: [claim.transaction], signer, requireClient, waitForArrival: async () => {}, }); if (claimSent === undefined) throw new Error('the claim was not sent'); const receipt = await requireClient(claim.transaction.chainId).waitForTransactionReceipt({ hash: claimSent.hash, }); if (receipt.status !== 'success') throw new Error(`the claim ${claimSent.hash} reverted`); const finalStatus = await waitForTransferStatus({ source, statusOptions, isDone: (status) => status.state === 'delivered', intervalMs: 15_000, }); return { crossingHash: bridge.hash, claimHash: claimSent.hash, finalStatus }; } }; ``` --- # Example file: shared.ts Source: https://next.gibs.finance/docs/examples/shared.ts.txt ```ts /** * Helpers both wallet examples share: the signer the examples expect, one viem * client per chain from the wallet's own RPC endpoints, an allowance reader, * a sender that honours `waitForReceipt`, and a status poller. * * The packages do not depend on web3.js, so the signer is a small * interface. A web3.js v4 adapter for it is shown in a comment below. * * These files are typechecked only. They are never run against a live chain. */ import { type FetchTransferStatusOptions, fetchTransferStatus, type TransferSource, type TransferStatus, } from '@gibs/bridge-indexer'; import type { AllowanceReader, PlannedTx } from '@gibs/bridge-sdk/execution'; import { type Chain, createPublicClient, erc20Abi, type Hex, http, type PublicClient, zeroAddress } from 'viem'; import { mainnet, pulsechain } from 'viem/chains'; /** One transaction for the wallet to sign and broadcast. */ export type WalletTransactionRequest = { readonly chainId: number; readonly from: Hex; readonly to: Hex; readonly data: Hex; readonly value: bigint; /** Set only when the plan names a gas limit. Otherwise the wallet estimates it. */ readonly gas?: bigint; }; /** * The signer the examples expect. It broadcasts one transaction on the chain * the request names and resolves with the transaction hash, before the * transaction is mined. The examples wait for receipts themselves, through * the viem clients, only where the plan says `waitForReceipt`. * * A web3.js v4 adapter, with one `Web3` instance per chain: * * ```ts * import { Web3 } from 'web3'; * * const web3ByChainId = new Map([[1, ethereumWeb3], [369, pulsechainWeb3]]); * const signer: WalletSigner = { * address: account, * sendTransaction: (request) => * new Promise((resolve, reject) => { * const web3 = web3ByChainId.get(request.chainId); * if (web3 === undefined) return reject(new Error(`no web3 for chain ${request.chainId}`)); * // web3.eth.sendTransaction returns a PromiEvent. Awaiting it resolves * // with the mined receipt; the 'transactionHash' event fires first. * web3.eth * .sendTransaction({ * from: request.from, * to: request.to, * data: request.data, * value: request.value, * ...(request.gas === undefined ? {} : { gas: request.gas }), * }) * .on('transactionHash', (hash) => resolve(hash as Hex)) * .on('error', reject); * }), * }; * ``` */ export type WalletSigner = { readonly address: Hex; readonly sendTransaction: (request: WalletTransactionRequest) => Promise; }; /** The wallet's own RPC endpoint for each chain it supports, by decimal chain id. */ export type RpcUrlByChainId = ReadonlyMap; /** The chains these examples touch. A real wallet lists every chain it supports. */ const viemChainById: ReadonlyMap = new Map([ [mainnet.id, mainnet], [pulsechain.id, pulsechain], ]); /** * Builds one viem public client per chain, from the wallet's own endpoints. * Never the demo endpoints (`demoRpcUrls` in `@gibs/bridge-sdk/chains`). * * @param rpcUrlByChainId - the wallet's endpoint for each chain * @returns a lookup that answers null for a chain the wallet has no endpoint for */ export const createClientLookup = ( rpcUrlByChainId: RpcUrlByChainId, ): ((chainId: number) => PublicClient | null) => { const clients = new Map(); for (const [chainId, url] of rpcUrlByChainId) { const chain = viemChainById.get(chainId); if (chain === undefined) continue; clients.set(chainId, createPublicClient({ chain, transport: http(url) })); } return (chainId) => clients.get(chainId) ?? null; }; /** * Wraps a client lookup so that a missing chain fails fast with a clear message. * * @param clientForChain - the lookup from {@link createClientLookup} * @returns a lookup that throws for a chain with no client */ export const requireClientWith = (clientForChain: (chainId: number) => PublicClient | null) => (chainId: number): PublicClient => { const client = clientForChain(chainId); if (client === null) throw new Error(`the wallet has no RPC endpoint for chain ${chainId}`); return client; }; /** * An allowance reader for `planCandidateExecution`, on the wallet's own clients. * * @param requireClient - see {@link requireClientWith} * @returns the reader */ export const allowanceReaderWith = (requireClient: (chainId: number) => PublicClient): AllowanceReader => (chainId, token, owner, spender) => requireClient(chainId).readContract({ address: token, abi: erc20Abi, functionName: 'allowance', args: [owner, spender], }); /** * Reads what an account holds of one token, or of the native coin when the * token is the zero address. The examples read it before and after a * crossing, so a later section spends what actually arrived. * * @param options.client - a client on the token's chain * @param options.token - the token, or the zero address for the native coin * @param options.owner - the account * @returns the balance, in the token's base units */ export const readBalance = (options: { readonly client: PublicClient; readonly token: string; readonly owner: Hex; }): Promise => { const { client, token, owner } = options; if (token.toLowerCase() === zeroAddress) return client.getBalance({ address: owner }); return client.readContract({ address: token as Hex, abi: erc20Abi, functionName: 'balanceOf', args: [owner] }); }; /** One transaction the sender broadcast, with its hash. */ export type SentTransaction = { readonly transaction: PlannedTx; readonly hash: Hex }; /** * Sends a plan's transactions in order. * * - It waits for a receipt only where the plan sets `waitForReceipt`. * - A receipt is not arrival. When the next transaction is on another chain, * it calls `waitForArrival` with the transaction that started the crossing, * and sends nothing more until that resolves. * * @param options.transactions - the plan's transactions, in plan order * @param options.signer - the wallet's signer * @param options.requireClient - see {@link requireClientWith} * @param options.waitForArrival - resolves once the crossing's funds arrived on the next chain * @returns every transaction sent, with its hash, in order * @throws when a receipt reports a revert */ export const sendPlannedTransactions = async (options: { readonly transactions: readonly PlannedTx[]; readonly signer: WalletSigner; readonly requireClient: (chainId: number) => PublicClient; readonly waitForArrival: (crossing: SentTransaction) => Promise; }): Promise => { const { transactions, signer, requireClient, waitForArrival } = options; const sent: SentTransaction[] = []; for (const [index, transaction] of transactions.entries()) { const hash = await signer.sendTransaction({ chainId: transaction.chainId, from: signer.address, to: transaction.to, data: transaction.data, value: transaction.value, ...(transaction.gasLimit === null ? {} : { gas: transaction.gasLimit }), }); sent.push({ transaction, hash }); if (!transaction.waitForReceipt) continue; const receipt = await requireClient(transaction.chainId).waitForTransactionReceipt({ hash }); if (receipt.status !== 'success') { throw new Error( `${transaction.kind} transaction ${hash} reverted on chain ${transaction.chainId}`, ); } const next = transactions[index + 1]; if (next !== undefined && next.chainId !== transaction.chainId) { await waitForArrival({ transaction, hash }); } } return sent; }; /** * Waits for a time, or rejects when the signal fires. * * @param milliseconds - how long to wait * @param signal - cancels the wait */ export const sleep = (milliseconds: number, signal?: AbortSignal): Promise => new Promise((resolve, reject) => { if (signal?.aborted) return reject(signal.reason); const timer = setTimeout(resolve, milliseconds); signal?.addEventListener( 'abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true }, ); }); /** A transfer that ended badly. Waiting longer does not change it. */ export class TransferEndedError extends Error { constructor(readonly status: TransferStatus) { super(`the transfer ended as ${status.state}`); this.name = 'TransferEndedError'; } } /** * Polls `fetchTransferStatus` until `isDone` accepts an answer. * * @param options.source - the transaction that started the transfer * @param options.statusOptions - passed through to `fetchTransferStatus` * @param options.isDone - true for the answer the caller waits for * @param options.intervalMs - the pause between polls * @returns the first answer `isDone` accepts * @throws {TransferEndedError} on `failed` or `stranded` */ export const waitForTransferStatus = async (options: { readonly source: TransferSource; readonly statusOptions: FetchTransferStatusOptions; readonly isDone: (status: TransferStatus) => boolean; readonly intervalMs: number; }): Promise => { const { source, statusOptions, isDone, intervalMs } = options; for (;;) { const status = await fetchTransferStatus(source, statusOptions); if (status.state === 'failed' || status.state === 'stranded') throw new TransferEndedError(status); if (isDone(status)) return status; await sleep(intervalMs, statusOptions.signal); } }; /** * The unix-seconds deadline a PulseX swap carries, `seconds` from now. * * @param seconds - how long the swap stays valid * @returns the deadline */ export const deadlineIn = (seconds: number): bigint => BigInt(Math.floor(Date.now() / 1000) + seconds); ``` --- # Example file: tsconfig.json Source: https://next.gibs.finance/docs/examples/tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM"], "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noUnusedLocals": true, "noUnusedParameters": true, "noEmit": true, "skipLibCheck": true, "isolatedModules": true, "types": [] }, "include": ["*.ts"] } ``` --- Source: https://next.gibs.finance/docs/packages/abis/llms.txt # @gibs/abis > Contract interfaces for the Gibs Finance bridge, as viem-ready constants. > Each export is an ABI built with viem's `parseAbi`. Pass it to > `readContract`, `writeContract` or `getContract` and viem infers the types. ## Install - Run `npm install @gibs/abis viem`. - It runs on Node 20 or later, in browsers, and in React Native under Hermes. - `import` loads the ECMAScript module build (`dist/`). `require` loads the CommonJS build (`dist/cjs/`). - React Native 0.77 or later: Metro's default configuration and the React Native jest preset load it with no extra setup. Add no `resolveRequest` hook and no `transformIgnorePatterns`. The `react-native` condition gives an `import` statement the ECMAScript module build and a `require` call the CommonJS build. - viem is a peer dependency. The range is `>=2.35.0 <3`. - Version note: 1.12.0 and older on npm are broken. 1.13.0 is the first usable version. ## Entry point The package has one entry point, `@gibs/abis`. It exports `inputBridge`, `erc677`, `outputRouter`, `outputBridge`, `feeManager`, `nativeRouter`, `univ2Router` and related constants. ```ts import { createPublicClient, http, type Address } from 'viem' import { mainnet } from 'viem/chains' import { inputBridge } from '@gibs/abis' const client = createPublicClient({ chain: mainnet, transport: http() }) /** Reads the smallest amount of a token the bridge accepts in one transfer. */ export const minimumPerTransfer = (bridge: Address, token: Address) => client.readContract({ address: bridge, abi: inputBridge, functionName: 'minPerTx', args: [token], }) ``` ## Rules - The ABIs hold only the functions and events the bridge interface uses. They are not full contract ABIs. - Some bridges take an extra sender-origin argument. Use the `*ExtraInput` ABIs (`inputBridgeExtraInput`, `erc677ExtraInput`) for those. - An ABI does not give you addresses. Read addresses from `@gibs/bridge-sdk/config` (`pathway`). ## Links - [README](https://www.npmjs.com/package/@gibs/abis) - [@gibs/bridge-sdk llms.txt](https://next.gibs.finance/docs/packages/bridge-sdk/llms.txt) - [Gibs Finance llms.txt](https://next.gibs.finance/llms.txt): the whole platform. --- Source: https://next.gibs.finance/docs/packages/bridge-client/llms.txt # @gibs/bridge-client > Browser-side endpoints, cached viem public clients, token metadata reads > and chain logo URLs for a Gibs Finance bridge interface. It has no > framework dependency. It sits between `@gibs/bridge-sdk` (which prices and > builds transfers) and an application that renders them. ## Install - Run `npm install @gibs/bridge-client viem`. - `import` loads the ECMAScript module build (`dist/`). `require` loads the CommonJS build (`dist/cjs/`). - React Native 0.77 or later: Metro's default configuration and the React Native jest preset load every entry point with no extra setup. Add no `resolveRequest` hook and no `transformIgnorePatterns` for these packages. - It needs no polyfill, not even for `URL`: React Native 0.77's own `URL` and `URLSearchParams` are partial, and no entry point reads them. - It needs Node 20 or later, a modern browser, or React Native 0.77 or later under Hermes. - viem is a peer dependency. The range is `>=2.35.0 <3`. - Version note: 1.12.0 and older on npm are broken. Use 1.13.0 or later. ## Entry points - `@gibs/bridge-client/client`: `clientFromChain(chainId)`, a cached viem public client per chain. - `@gibs/bridge-client/endpoints`: per-chain RPC endpoint lists with storage and subscription. - `@gibs/bridge-client/networks`: the supported viem chains. - `@gibs/bridge-client/image`: chain logo URLs. - `@gibs/bridge-client/token-metadata`: token metadata reads and caching. - `@gibs/bridge-client/chain-read`: allowance reads. - `@gibs/bridge-client/cache`: the `Cache` class. ## Example ```ts import { clientFromChain } from '@gibs/bridge-client/client' import { chainLogoUrl } from '@gibs/bridge-client/image' import { isSupportedEvmChain } from '@gibs/bridge-client/networks' const pulsechain = 369 if (isSupportedEvmChain(pulsechain)) { const client = clientFromChain(pulsechain) const blockNumber = await client.getBlockNumber() console.log(blockNumber, chainLogoUrl({ chainId: pulsechain, displayPx: 32 })) } ``` ## Rules - Check `isSupportedEvmChain(chainId)` first. `clientFromChain` throws for a chain outside the supported list. - `clientFromChain` takes a chain id number. It is not the same function as `clientFromChain` in `@gibs/common`, which takes a viem chain and a list of URLs. - The client reads its URLs from the endpoint table. An empty entry for a chain falls back to the chain defaults. For the five bridge chains those defaults are `demoRpcUrlsFor` from `@gibs/bridge-sdk/chains`, which put the shared valve.city `vk_demo` key first. They are for demos only: set your own endpoints with `setEndpoints` in production, or pass your own viem clients to the readers. - A chain logo can come from gib.show or from Relay's CDN. Use `chainLogoUrl`. Do not build the URL yourself. - This package reads. It never signs or broadcasts. ## Links - [README](https://www.npmjs.com/package/@gibs/bridge-client) - [@gibs/bridge-sdk llms.txt](https://next.gibs.finance/docs/packages/bridge-sdk/llms.txt) - [Gibs Finance llms.txt](https://next.gibs.finance/llms.txt): the whole platform. --- Source: https://next.gibs.finance/docs/packages/bridge-indexer/llms.txt # @gibs/bridge-indexer > A reader for the Gibs Finance bridge indexer. It gives you the transfer > archive, delivery receipts and live crossing status as data. It only > reads. It never signs, broadcasts or renders. ## Install - Run `npm install @gibs/bridge-indexer viem`. - `import` loads the ECMAScript module build (`dist/`). `require` loads the CommonJS build (`dist/cjs/`). - React Native 0.77 or later: Metro's default configuration and the React Native jest preset load every entry point with no extra setup. Add no `resolveRequest` hook and no `transformIgnorePatterns` for these packages. - It needs no polyfill, not even for `URL`: React Native 0.77's own `URL` and `URLSearchParams` are partial, and no entry point reads them. - It needs Node 20 or later, or React Native 0.77 or later under Hermes. - viem is a peer dependency. The range is `>=2.35.0 <3`. - Version note: 1.13.0 is the first version on npm. - The package ships `schema.graphql`, a snapshot of the indexer schema. ## Entry points - `@gibs/bridge-indexer`: everything below. - `@gibs/bridge-indexer/endpoint`: `setIndexerEndpoint`, `indexerEndpoint`, `indexerClient`, `DEFAULT_INDEXER_ENDPOINT`. - `@gibs/bridge-indexer/transfers`: `fetchBridgeTransactions`, `buildBridgeFilter`, `fetchDeliveredMessageHashes`. - `@gibs/bridge-indexer/bridge-status`: the status names (`SUBMITTED` to `DELIVERED`). - `@gibs/bridge-indexer/live-status`: `liveBridgeStatusStageOne`, `liveBridgeStatusStageTwo`. - `@gibs/bridge-indexer/graphql`: generated GraphQL types. ## Endpoint The default endpoint is `https://next-indexer.gibs.finance`. Importing the package has no side effects. The client is created on first use. Call `setIndexerEndpoint` before the first query to read another indexer. ```ts import { DEFAULT_INDEXER_ENDPOINT, indexerEndpoint, setIndexerEndpoint } from '@gibs/bridge-indexer' setIndexerEndpoint('http://localhost:42069/graphql') const inForce: string = indexerEndpoint() const fallback: string = DEFAULT_INDEXER_ENDPOINT ``` The indexer serves GraphQL at `/` and `/graphql`. `GET /version` returns the indexer version, the schema name and the git commit. Production `https://indexer.gibs.finance` can lag the schema. If a query fails there with an unknown field, use the next indexer. ## Read a wallet's transfers ```ts import { fetchBridgeTransactions } from '@gibs/bridge-indexer' const controller = new AbortController() const page = await fetchBridgeTransactions( { address: '0x0000000000000000000000000000000000000001', filterMode: 'all', limit: 10, }, controller, ) if (page) { page.totalCount } ``` `filterMode` is `'pending'` (the default), `'completed'` or `'all'`. A transaction hash, message id or message hash in `hash` overrides the address. ## One transfer's status in one call `fetchTransferStatus` takes the transaction that started a transfer. It returns one state from the bridge's state table, whether anybody can release it now, the destination transaction, and the aggregator's own answer. ```ts import { fetchTransferStatus, transferStateNames } from '@gibs/bridge-indexer' import type { PublicClient } from 'viem' declare const ethereumClient: PublicClient const controller = new AbortController() const status = await fetchTransferStatus( { chainId: 1, txHash: '0x0000000000000000000000000000000000000000000000000000000000000001' }, { originClient: ethereumClient, signal: controller.signal }, ) const label: string = transferStateNames[status.state] if (status.state === 'ready-your-turn') { // Show a release button. No delivery service will execute this claim. } const settledBy: `0x${string}` | null = status.destinationTxHash ``` - States: `sent`, `confirmed-not-final`, `validators-confirming`, `ready-delivery-service`, `ready-your-turn`, `delivered`, `entry-reached`, `stranded`, `failed`, `unknown`. - `claimable` is true in both ready states. An attached delivery service can still never come. - Without `originClient`, a transaction the indexer has not recorded is `unknown`. With it, the answer is `sent` or `confirmed-not-final`. - A route that starts off an aggregator needs `aggregator`: `{ provider: 'lifi' }`, `{ provider: 'relay', requestId, apiKey }` or `{ provider: 'near-intents', depositAddress }`. A chain the bridge does not cross throws without it. - Pass your own `fetch` and `endpoint` if you need them. The endpoint defaults to `indexerEndpoint()`. - It works around every defect in the list below. You do not have to. ## Rules you must not break - Known production defects. A batched re-index will fix them. Until then, work around them. - Five relations are always `null`. Do not read them: `AMBBridge.validatorContract`, `Omnibridge.ambBridge`, `Omnibridge.validatorContract`, `UserRequest.destinationAMBBridge`, `UserRequest.destinationOmnibridge`. - The join from a request to its `Completion` or `Delivery` works in one direction only. Query `Completion` or `Delivery` by `messageHash`. - A message for a native pathway records the ERC-20 omnibridge, not the native one. - Read `delivered` (a boolean on the request). Do not read `delivery`. The `delivery` relation can be `null` for a request that landed. - Hex filters are lowercase. A checksummed address in a `where` clause matches nothing. Lowercase every address and hash first. - `FeeUpdate.fee` is a rate scaled by `1e18`. `10n ** 18n` is one hundred percent. - `UserRequest.feeAmount` is the fee actually taken, in base units of the origin token. `null` means no fee log was seen. - `BigInt` fields arrive as decimal strings. ## Links - [README](https://www.npmjs.com/package/@gibs/bridge-indexer) - [Indexer GraphQL](https://next-indexer.gibs.finance/graphql) - [Gibs Finance llms.txt](https://next.gibs.finance/llms.txt): the whole platform. --- Source: https://next.gibs.finance/docs/packages/bridge-sdk/llms.txt # @gibs/bridge-sdk > A TypeScript library that prices and builds transfers across the Gibs > Finance omnibridges (PulseChain with Ethereum, PulseChain with BNB Smart > Chain). It also builds the requests for the aggregator routes (LI.FI, > Relay, NEAR Intents) that reach those bridges from other chains. It does > not sign or send anything. You bring a viem client and a wallet. ## Install - Run `npm install @gibs/bridge-sdk viem`. - It runs on Node 20 or later (`engines.node` is `>=20`), in browsers, and in React Native under Hermes. - viem is a peer dependency. The range is `>=2.35.0 <3`. - Version note: 1.12.0 and older on npm are broken. 1.13.0 is the first usable version. ## Ways to use the package The `exports` map picks the build. No consumer configuration is needed. - ECMAScript modules: `import { pathway } from '@gibs/bridge-sdk/config'` loads `dist/config.js`. - CommonJS: `require('@gibs/bridge-sdk/config')` loads `dist/cjs/config.js`, with its own declaration files. - React Native 0.77 or later, default `metro.config.js`. Add no `resolveRequest` hook. In 0.79 or later, Metro reads `exports`: the `react-native` condition gives an `import` statement the ECMAScript module build and a `require` call the CommonJS build. The SDK then shares your build of viem, so Metro bundles viem once. - React Native jest preset: loads the CommonJS build. Do not add `transformIgnorePatterns` for these packages. - Metro with package exports off (the default in React Native 0.77 and 0.78): every subpath resolves through a folder the package ships for it, such as `config/package.json`, which points at the CommonJS build. Import the subpath as usual, never a `dist` path. - No entry point except `@gibs/bridge-sdk/ton` needs a polyfill. That includes a `URL` polyfill: React Native 0.77's own `URL` and `URLSearchParams` are partial, and no entry point reads them. ```ts import { Chains, pathway, Providers } from '@gibs/bridge-sdk/config' import { amountAfterBridgeFee, bridgeCost } from '@gibs/bridge-sdk/settings' const route = pathway([Providers.PULSECHAIN, Chains.PLS, Chains.ETH], true) const cost = bridgeCost({ amountToBridge: 10n ** 18n, bridgeFee: 3n * 10n ** 15n }) console.log(route?.from, amountAfterBridgeFee({ amountToBridge: 10n ** 18n, bridgeCost: cost })) ``` Bundle weight. Metro does not drop unused exports, so the entry point decides the weight. Measured in a React Native 0.87.1 production bundle that already holds viem: - `/config` and `/settings` (offline quoting): about 24 kB. - `/chain-info` (live fee reads) with the above: about 1.16 MB, because it loads `viem/chains` (1.12 MB). About 41 kB if the app already imports `viem/chains`. - The root entry point: about 1.29 MB. About 164 kB if the app already imports `viem/chains`. TON: - `@gibs/bridge-sdk/ton-connect`: TON Connect requests and address re-encoding. No TON library, no `Buffer`. - `@gibs/bridge-sdk/ton`: the same, plus `tonExternalMessageHash`, which needs `@ton/core`. Set `globalThis.Buffer` (the `buffer` package) before importing it in a browser or React Native. In React Native, Metro also needs `expo-crypto` installed, because `@ton/crypto` requires it there. - `@gibs/bridge-sdk/ton-execution`: the old name of `@gibs/bridge-sdk/ton`. ## Entry points Import from the narrowest subpath. The root `@gibs/bridge-sdk` exports everything except TON. - `@gibs/bridge-sdk/config`: chains, providers, pathways, native-asset tables. - `@gibs/bridge-sdk/types`: shared types such as `BridgeKey` and `Token`. - `@gibs/bridge-sdk/chains`: viem chain objects with logos and public RPC URLs. `demoRpcUrls` is for demos only. - `@gibs/bridge-sdk/chain-info`: on-chain reads (token mappings, fees, minimum amounts). - `@gibs/bridge-sdk/settings`: fee and amount arithmetic. - `@gibs/bridge-sdk/consolidated-quote`: LI.FI request builders and parsers. - `@gibs/bridge-sdk/relay-quote`: Relay request builders and parsers. - `@gibs/bridge-sdk/near-intents`: NEAR Intents requests and parsers. - `@gibs/bridge-sdk/pre-quote`: price checks before a binding quote. - `@gibs/bridge-sdk/routing`: route search across carriers. - `@gibs/bridge-sdk/execution`: execution plans (`planExecution`, `planCandidateExecution`, `PlannedTx`, `DeliveryChoice`, `AllowanceReader`, `assumeNoAllowance`). - `@gibs/bridge-sdk/claims`: manual claim transactions (`buildClaimTransaction`, `claimCall`, `packSignatures`, `signatureToVRS`, `encodeReleaseWithoutTipData`, `encodeReleaseToRouterData`). - `@gibs/bridge-sdk/image-links`: image URLs from gib.show. - `@gibs/bridge-sdk/abis`: contract ABIs. - `@gibs/bridge-sdk/ton-connect`: TON Connect requests, with no TON library. - `@gibs/bridge-sdk/ton`: TON Connect requests and the external message hash (needs `@ton/core`). ## Chains and pathways A chain is a hex id from the `Chains` enum. A route is a `BridgeKey`: `[provider, fromChain, toChain]`. `pathway` returns the contract addresses for a key, or `undefined` when no pathway exists. ```ts import { Chains, Providers, pathway } from '@gibs/bridge-sdk/config' import type { BridgeKey } from '@gibs/bridge-sdk/types' const bridgeKey: BridgeKey = [Providers.PULSECHAIN, Chains.ETH, Chains.PLS] const route = pathway(bridgeKey, true) // true: mainnet only if (route) { route.from // the mediator on Ethereum, where funds enter route.feeManager // 'to': the PulseChain mediator owns the fee manager } ``` ## Fees: direction and units These rules must hold in every integration. - PulseChain is the home side of both omnibridges. - Foreign to home (`feeF2H`) means entering PulseChain. - Home to foreign (`feeH2F`) means leaving PulseChain. - Use the fee for your direction. Do not use one fee for both. - Bridge fees are a rate per `1e18`. `1e18` is one hundred percent. - Aggregator referral fees are basis points. `10_000n` is one hundred percent. - LI.FI takes its fee as a decimal fraction (`0.001` is a tenth of a percent). - Fees are on-chain settings. Read them. Do not hard-code them. ```ts import { loadBridgeFees } from '@gibs/bridge-sdk/chain-info' import { Chains, pathway, Providers } from '@gibs/bridge-sdk/config' import { amountAfterBridgeFee, bridgeCost } from '@gibs/bridge-sdk/settings' import type { PublicClient } from 'viem' declare const ethereumClient: PublicClient declare const pulsechainClient: PublicClient const route = pathway([Providers.PULSECHAIN, Chains.ETH, Chains.PLS], true) if (route) { const { feeF2H } = await loadBridgeFees({ pathway: route, fromChainClient: ethereumClient, toChainClient: pulsechainClient, }) const amountToBridge = 1_000_000n const cost = bridgeCost({ amountToBridge, bridgeFee: feeF2H }) const received = amountAfterBridgeFee({ amountToBridge, bridgeCost: cost }) } ``` `loadBridgeFees` reads the default rate. It does not read per-token rates. ## Tokens that are not bridged yet `tokenBridgeInfo` asks both mediators how a token maps across the bridge. A `null` `assetOutAddress` is a normal answer. The token gets an address on the far side when its first transfer arrives. Always check for `null`. ```ts import { tokenBridgeInfo } from '@gibs/bridge-sdk/chain-info' import { Chains, Providers } from '@gibs/bridge-sdk/config' import type { Token } from '@gibs/bridge-sdk/types' import type { PublicClient } from 'viem' declare const assetIn: Token declare const ethereumClient: PublicClient declare const pulsechainClient: PublicClient const info = await tokenBridgeInfo({ bridgeKey: [Providers.PULSECHAIN, Chains.ETH, Chains.PLS], assetIn, isProd: true, fromChainClient: ethereumClient, toChainClient: pulsechainClient, }) if (info && info.assetOutAddress === null) { // Not bridged yet. Do not treat this as an error. } ``` ## Aggregator identity: pass your own The package ships no API key. Pass your own. - Relay: pass your own `apiKey` to `relayRequestHeaders`. Name your own `relayReferrer`. The package sends no referrer by default, because Relay rejects a referrer without a key. - LI.FI: set your own `lifiIntegrator`. The package sends no integrator by default. A positive `lifiFee` without your own integrator throws. - NEAR Intents: no key is needed. No `referral` is sent unless you pass one. The `User-Agent` names Gibs Finance unless you pass your own `userAgent`, or `null` for none. Browsers drop it; Node and React Native send it. ```ts import { buildLifiContractCallsRequest, type ConsolidatedQuoteRequest, } from '@gibs/bridge-sdk/consolidated-quote' import { nearIntentsRequestHeaders } from '@gibs/bridge-sdk/near-intents' import { buildRelayQuoteRequest, relayQuoteUrl, relayRequestHeaders } from '@gibs/bridge-sdk/relay-quote' declare const baseRequest: ConsolidatedQuoteRequest declare const myRelayApiKey: string const request: ConsolidatedQuoteRequest = { ...baseRequest, lifiIntegrator: 'your-app.example', lifiFee: 0.001, relayReferrer: 'your-app.example', } const lifiBody = buildLifiContractCallsRequest(request) const relayBody = buildRelayQuoteRequest(request) if (relayBody) { await fetch(relayQuoteUrl, { method: 'POST', body: JSON.stringify(relayBody), headers: relayRequestHeaders(myRelayApiKey), }) } const nearHeaders = nearIntentsRequestHeaders(undefined, { userAgent: 'your-app/1.0' }) ``` ## Fees: nothing is added for you - No builder adds a fee, a payout address or a Gibs Finance label. A request carries one only when you pass it. - Bridge fee: on-chain, on every direct crossing. Read it with `loadBridgeFees`. You cannot turn it off. - Delivery fee: only with `shouldDeliver: true` and a fee director. See it with `deliveryFee`. - LI.FI fee: only with `lifiFee` and your own `lifiIntegrator`. - Relay application fee: only with `relayAppFee: { recipient, basisPoints }`. - Provider fees (gas, solver, protocol): charged by LI.FI, Relay or NEAR Intents. Read them with `lifiQuoteFees`, `relayQuoteFees` and the NEAR Intents quote parser. - NEAR Intents adds 25 basis points to a quote sent without a partner key. - `@gibs/bridge-sdk/referral` holds Gibs Finance's own values (`referralIntegrator`, `relayReferralRecipient`, `defaultReferralFeeBasisPoints`). They pay Gibs Finance. Do not pass them from your own app. ## RPC endpoints: the defaults are for demos only - Every reader takes your viem clients. None builds its own. - `chainsMetadata` carries public endpoints only. - `demoRpcUrls` and `demoRpcUrlsFor` add a shared valve.city key, `vk_demo`. They are for demos only. The key is public, but its rate budget is shared and the Gibs Finance operator runs the proxy. - In production, use your own RPC provider. ```ts import { demoRpcUrlsFor } from '@gibs/bridge-sdk/chains' import { createPublicClient, fallback, http } from 'viem' import { mainnet } from 'viem/chains' const ethereumClient = createPublicClient({ chain: mainnet, transport: http('https://your-rpc.example') }) const demoOnly = createPublicClient({ chain: mainnet, transport: fallback(demoRpcUrlsFor(mainnet.id).map((url) => http(url))), }) ``` ## Nulls and throws - A question the package cannot answer returns `null`, `undefined` or `false`. - A caller mistake or a failed chain read throws. - Every request builder returns `null` for a request its provider cannot express. - Every response parser returns `null` for an unexpected response shape. - Check each return value before you use it. ## Links - [Integrate Gibs into a wallet or app](https://next.gibs.finance/docs/): search, plan, send, track and claim, with two complete examples. The same guide ships in this package as `docs/integration-guide.md`. - [README](https://www.npmjs.com/package/@gibs/bridge-sdk): the full reference. - [Gibs Finance llms.txt](https://next.gibs.finance/llms.txt): the whole platform. - [@gibs/quotes](https://next.gibs.finance/docs/packages/quotes/llms.txt): live multi-provider quotes. --- Source: https://next.gibs.finance/docs/packages/common/llms.txt # @gibs/common > Low-level blockchain helpers shared across the Gibs Finance packages: > cached viem clients with fallback URLs, Multicall3 batching, ERC-20 > metadata reads, memoizing caches and bigint-safe JSON. Nothing in it is > specific to the bridge. ## Install - Run `npm install @gibs/common viem`. - It runs on Node 20 or later, in browsers, and in React Native under Hermes. - `import` loads the ECMAScript module build (`dist/`). `require` loads the CommonJS build (`dist/cjs/`). - React Native 0.77 or later: Metro's default configuration and the React Native jest preset load it with no extra setup. Add no `resolveRequest` hook and no `transformIgnorePatterns`. The `react-native` condition gives an `import` statement the ECMAScript module build and a `require` call the CommonJS build. - It needs no polyfill, not even for `URL`: React Native 0.77's own `URL` and `URLSearchParams` are partial, and no entry point reads them. - Import from the narrowest subpath, for example `@gibs/common/cache`. Metro does not drop unused exports. - It has no runtime dependency except viem. - viem is a peer dependency. The range is `>=2.35.0 <3`. - Version note: 1.12.0 and older on npm are broken. 1.13.0 is the first usable version. ## Entry points - `@gibs/common`: everything below. - `@gibs/common/client`: `clientFromChain({ chain, urls })`, `chainKey`, `clientCache`. - `@gibs/common/erc20`: `multicallErc20`, `erc20MetadataCalls`. - `@gibs/common/multicall`: `multicallRead`. - `@gibs/common/cache`: `maxMemoize`, `ttlMemoizeSingle`, `memoizeByKey`. - `@gibs/common/serialize`: `jsonAnyStringify`, `jsonAnyParse`, `isSerializedBigInt`. - `@gibs/common/types`: `Call`, `Erc20Metadata`. - `@gibs/common/url`: `buildUrl`, `buildQuery`, `parseQuery`, `setQueryParameter`, `splitUrl`, `isHttpUrl`, `encodeQueryComponent`, `decodeQueryComponent`. They build and read request addresses with string work, never with `URL`, and encode a query exactly as `URLSearchParams` does. ## Read a token's name, symbol and decimals ```ts import { clientFromChain } from '@gibs/common/client' import { multicallErc20 } from '@gibs/common/erc20' import { mainnet } from 'viem/chains' const client = clientFromChain({ chain: mainnet, urls: ['https://ethereum-rpc.publicnode.com'] }) const [name, symbol, decimals] = await multicallErc20({ client, chain: mainnet, target: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }) ``` ## Keep a bigint through JSON ```ts import { jsonAnyParse, jsonAnyStringify } from '@gibs/common/serialize' const text = JSON.stringify({ balance: 10n ** 18n }, jsonAnyStringify) const restored = JSON.parse(text, jsonAnyParse) ``` ## Memoize ```ts import { maxMemoize, ttlMemoizeSingle } from '@gibs/common/cache' const square = maxMemoize((x: number) => x * x, 100) const price = ttlMemoizeSingle(async (symbol: string) => symbol, 30_000) ``` `memoizeByKey` keys each answer by a resolver you supply, and exposes the cache so you can evict one answer or clear them all. ```ts import { memoizeByKey } from '@gibs/common/cache' const decimals = memoizeByKey( async ({ chainId, address }: { chainId: number; address: string }) => (chainId === 1 ? 6 : 18), ({ chainId, address }) => `${chainId}-${address.toLowerCase()}`, ) decimals.cache.delete('1-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') decimals.cache.clear() ``` ## Rules - `multicallRead` and `multicallErc20` need a chain object that carries a Multicall3 deployment. A chain without one fails. - `clientFromChain` caches by chain id and URL list. The same URLs in a different order make a different client. - `@gibs/common/client` takes a viem chain and URLs. `@gibs/bridge-client/client` takes a chain id. They are different functions. - `multicallErc20` memoizes its answers. A repeat call with the same inputs does not read the chain again. - The `@gibs/common/test-utils/fuzz` subpath is for tests inside the source repository. It is not in the npm package. ## Links - [README](https://www.npmjs.com/package/@gibs/common) - [Gibs Finance llms.txt](https://next.gibs.finance/llms.txt): the whole platform. --- Source: https://next.gibs.finance/docs/packages/quotes/llms.txt # Cross-Chain Quote Client > An isomorphic TypeScript library that asks several cross-chain bridge > aggregators for live quotes at once, merges the answers, and helps you > route a transfer for the best delivered amount at the fewest wallet > signatures. It runs unchanged in a browser tab or on a Node server. It > carries no host name, no chain policy, and no secret of its own — you > supply all three through one configuration object. ## Quick orientation - This package runs in three modes: **from your own backend** (with real provider keys), **straight from a browser** (with no secret, or a public identifier only), or **against your own hosted quote service, with a browser fallback**. Pick one mode below and use its configuration shape. - It also runs in React Native 0.77 or later with no polyfill, not even for `URL`: React Native's own `URL` and `URLSearchParams` are partial, and no entry point reads them. - The wire contract below (`POST /v1/display-quotes`) is what a hosted quote service and a direct-from-browser client both speak. If you run your own service with the server-side pieces of this package, your server and your browser client already agree on the same request and response shapes. - A short list of rules never bend. Read them before you build anything that signs a transaction from a quote this library returns. ## Install Add the package as a dependency with your usual package manager, then import from its root: ```ts import { createQuoteClient, type QuoteClientConfig } from '' ``` Replace `` with the name you installed this under — you already know it from your own `package.json` dependency entry. ## Configuration: `QuoteClientConfig` Every fact about who is asking — provider keys, allowed origins, chain policy — lives in one object. Nothing else in this library carries a default that assumes a particular deployment. ```ts type QuoteClientConfig = { providers?: { lifi?: { apiKey?: string; integrator?: string; baseUrl?: string; fee?: number } relay?: { apiKey?: string; referrer?: string; baseUrl?: string } nearIntents?: { apiKey?: string; referral?: string; baseUrl?: string } } serviceUrl?: string fallback: 'direct' | 'none' probeAddresses?: Partial> neverSendChainIds?: readonly number[] cache?: { ttlMs?: number; maxEntries?: number } limits?: { concurrencyByCarrier?: Record; defaultRetryAfterMs?: number } fetch?: typeof fetch now?: () => number } ``` `fallback` is required on purpose. Every caller states what happens when `serviceUrl` cannot be reached, instead of inheriting a silent default. ## Mode 1: your own backend, with real secrets Run this on a server you control. Provider keys stay on that server and are never sent to a browser. ```ts const client = createQuoteClient( { providers: { lifi: { apiKey: process.env.LIFI_API_KEY, integrator: 'your-app' }, relay: { apiKey: process.env.RELAY_API_KEY }, }, fallback: 'none', neverSendChainIds: [/* any chain id your own bridge must never expose to an aggregator */], }, { carriers: [/* built-in and/or your own Carrier implementations — see below */] }, ) const response = await client.displayQuotes({ asks: [/* ... */] }) ``` ## Mode 2: straight from the browser, no service Send no secret to the browser. Omit every `providers.*.apiKey`, or send only a public, rate-limit-budget identifier — the same way a browser-safe value already works today. Leave `serviceUrl` unset. ```ts const client = createQuoteClient( { providers: { relay: { referrer: 'your-app.example' } }, fallback: 'none', }, { carriers: [/* your carriers */] }, ) ``` Every request this client makes goes straight from the visitor's browser to each provider. ## Mode 3: your own hosted service, with a browser fallback Point the browser at a service you run — built from this same package's server-side pieces — and fall back to Mode 2 automatically when that service cannot be reached. ```ts const client = createQuoteClient({ serviceUrl: 'https://quotes.your-app.example', fallback: 'direct', providers: { relay: { referrer: 'your-app.example' } }, // used only during a fallback }) ``` `fallback: 'direct'` opens a 60-second breaker after one failed call to `serviceUrl`, so a flapping service does not fail every single request on its way down; `fallback: 'none'` instead answers every ask with a `could-not-ask` refusal and never falls back to a direct call. ## Wire contract: `POST /v1/display-quotes` If you run your own service (Mode 3), have it answer this exact shape — the browser client this package builds already speaks it. Request body: `{ "asks": DisplayAsk[] }`, 1 to 24 asks, answered in order. ```ts type DisplayAsk = { from: { chainId: number; token: string } to: { chainId: number; token: string } amount: string // decimal string, base units of from.token recipientEcosystem?: 'evm' | 'svm' | 'bvm' | 'tvm' | 'tonvm' | 'hypevm' | 'zcash' // default: derived from to.chainId providers?: ('lifi' | 'relay' | 'near-intents')[] // default: every eligible provider precision?: 'estimate-ok' | 'exact' // default: 'estimate-ok' } ``` Response: `{ "answers": [{ "providers": { lifi?, relay?, "near-intents"? } }] }`, one answer per ask, in ask order. Each provider slot is one of three shapes: - **`quoted`** — `basis: 'exact'` or `basis: 'scaled-from-nearby-amount'`, plus `askedAmount`, `expectedAmount`, an optional `minimumAmount`, `quotedAt`, `ageMs`, and `cache: 'miss' | 'hit' | 'joined'`. A `scaled-from-nearby-amount` answer also carries `scaledFrom`: the real amount and the real answer it was scaled from. - **`refused`** — `kind: 'no-quote' | 'could-not-ask' | 'cannot-carry' | 'invalid-api-key'`, a `reason` (or null), and `minimumAmount` when the provider named one. - **`pending`** — the provider's own call is still running. Retry after `retryAfterMs`. The call keeps running in the background and fills the cache for the next ask that asks the same question. `precision: 'exact'` asks for a genuine answer to the exact amount you asked for. It never accepts a scaled answer — see "Rules that never bend," below. ## Route search: carriers The route-search engine asks every registered *carrier* — a built-in aggregator (LI.FI, Relay, NEAR Intents) or a local read you register yourself (a direct on-chain swap, your own bridge contract) — what it can deliver, then scores every combination it can build for the best delivered amount at the fewest wallet signatures. Register your own carrier by implementing the `Carrier` interface: ```ts const myOwnCarrier: Carrier = { id: 'my-carrier', edgesFrom: (from, to) => [/* every edge this carrier can move funds across right now */], isRateLimited: false, // true for anything that calls a rate-limited network API ask: async ({ edge, inputAmount }) => { // return { ok: true, deliveredAmount, minimumDeliveredAmount } or // { ok: false, refusal: { kind, reason, minimumAmount } } }, // fusesWithNext?: (nextHop) => boolean — set this only when arriving via // this carrier ALSO executes the next hop, so the two cost one signature. } ``` "Fewest signatures for the most delivered" is the engine's actual scoring order, not a slogan: it maximizes delivered amount first, then minimizes how many wallet signatures a route costs among routes that tie. A carrier that fuses two hops into a single signature beats an otherwise-equal route that costs an extra step. Every network call a carrier makes — built in or your own — passes through this library's shared guards (a keyed concurrency limiter, a Retry-After cooldown, and your `neverSendChainIds` tripwire) before it ever reaches `fetch`, once you wrap it through the client's upstream funnel. ## Rules that never bend - **`neverSendChainIds` is enforced everywhere, including forwarding.** If you run a chain that must never reach a public aggregator, name its id here. Every outbound call this library makes, direct or forwarded, refuses any request naming it. - **A binding quote is never cached or shared.** A quote that reserves a real user address, real calldata, or a live deposit address answers fresh every time — never from this library's own cache. - **A scaled figure never reaches an execution builder.** `expectedAmount` and `minimumAmount` on a `basis: 'scaled-from-nearby-amount'` answer carry a distinct type brand (`EstimatedDelivery`) from a genuine `basis: 'exact'` answer (`ExactAmount`). The function that decodes an amount for spending only accepts the exact brand, so mixing them up is a compile error, not a rule you have to remember to check. - **Provider keys never run in a browser.** If your frontend needs a display quote and you hold a real key, put it behind your own service (Mode 3) — never Mode 2 with a real `apiKey` set on a browser build. - **Verify a route's execution calldata before signing it**, independent of what a display quote showed. A display quote exists to show a reader what to expect; it is never the thing that gets signed. Build and check the real execution transaction separately, immediately before asking a wallet to sign it.