|
| 1 | +# @orbital-stellar/anchor-sdk |
| 2 | + |
| 3 | +**Typed clients for talking to Stellar anchors.** Discovery, SEP-10 authentication, SEP-12 KYC, SEP-24 interactive deposit/withdraw, and SEP-31 cross-border payments - each with a validated request/response shape instead of hand-rolled `fetch` calls. |
| 4 | + |
| 5 | +```bash |
| 6 | +pnpm add @orbital-stellar/anchor-sdk |
| 7 | +``` |
| 8 | + |
| 9 | +## What it does |
| 10 | + |
| 11 | +`anchor-sdk` is the client side of the SEP anchor protocols. You point it at an anchor's home domain, it discovers the endpoints from `stellar.toml`, authenticates, and drives the deposit/withdraw or cross-border payment flow - each response validated against a `zod` schema so a malformed anchor reply throws instead of silently propagating `undefined`. |
| 12 | + |
| 13 | +It never holds a Stellar secret key. Every flow that needs a signature takes a caller-supplied signing callback, so the key can live in a hardware wallet, a KMS, or wherever the consumer already keeps it - the SDK only ever sees signed XDR. |
| 14 | + |
| 15 | +## SEP-1 - discovery |
| 16 | + |
| 17 | +```ts |
| 18 | +import { discoverAnchor } from "@orbital-stellar/anchor-sdk"; |
| 19 | + |
| 20 | +const toml = await discoverAnchor("anchor.example.com"); |
| 21 | +// toml.WEB_AUTH_ENDPOINT, toml.TRANSFER_SERVER_SEP0024, toml.SIGNING_KEY, ... |
| 22 | +``` |
| 23 | + |
| 24 | +`discoverAnchor` fetches `https://{homeDomain}/.well-known/stellar.toml`, caps the response at 100 KB, and parses only the top-level keys this SDK understands. A response that isn't reachable, isn't valid, or exceeds the size cap throws `Sep1DiscoveryError`. |
| 25 | + |
| 26 | +## SEP-10 - authentication |
| 27 | + |
| 28 | +```ts |
| 29 | +import { discoverAnchor, Sep10Client } from "@orbital-stellar/anchor-sdk"; |
| 30 | +import { Keypair } from "@stellar/stellar-sdk"; |
| 31 | + |
| 32 | +const toml = await discoverAnchor("anchor.example.com"); |
| 33 | +const client = Sep10Client.fromToml(toml, "anchor.example.com"); |
| 34 | + |
| 35 | +const keypair = Keypair.fromSecret(process.env.STELLAR_SECRET!); |
| 36 | + |
| 37 | +const token = await client.authenticate({ |
| 38 | + account: keypair.publicKey(), |
| 39 | + sign: async (challenge) => { |
| 40 | + // Sign with whatever holds your key - here, an in-process Keypair. |
| 41 | + const tx = /* build a Transaction from challenge.transaction */; |
| 42 | + tx.sign(keypair); |
| 43 | + return tx.toXDR(); |
| 44 | + }, |
| 45 | +}); |
| 46 | +``` |
| 47 | + |
| 48 | +Every challenge is validated against the anchor's `SIGNING_KEY`, network passphrase, home domain, and `web_auth_domain` **before** it reaches your `sign` callback - a hostile or compromised anchor cannot get an arbitrary transaction signed by handing you a "challenge" that is actually a payment or a `set_options` adding a signer. `Sep10Client.fromToml` is the preferred constructor: it's the path that cannot forget to pass `SIGNING_KEY`, without which no challenge can be attributed to the anchor. |
| 49 | + |
| 50 | +## SEP-12 - KYC |
| 51 | + |
| 52 | +```ts |
| 53 | +import { Sep12Client } from "@orbital-stellar/anchor-sdk"; |
| 54 | + |
| 55 | +const kyc = new Sep12Client(toml.KYC_SERVER!); |
| 56 | +const info = await kyc.getCustomer({ account: keypair.publicKey() }, token); |
| 57 | + |
| 58 | +if (info.status === "NEEDS_INFO") { |
| 59 | + const form = new FormData(); |
| 60 | + form.set("first_name", "Jane"); |
| 61 | + form.set("last_name", "Doe"); |
| 62 | + await kyc.putCustomer(form, token); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +## SEP-24 - interactive deposit / withdraw |
| 67 | + |
| 68 | +```ts |
| 69 | +import { Sep24Client, Sep24StatusMachine } from "@orbital-stellar/anchor-sdk"; |
| 70 | + |
| 71 | +const transfer = new Sep24Client(toml.TRANSFER_SERVER_SEP0024!); |
| 72 | + |
| 73 | +const { url, id } = await transfer.initiateDeposit( |
| 74 | + { asset_code: "USDC" }, |
| 75 | + token, |
| 76 | +); |
| 77 | +// Open `url` in a webview so the user completes the anchor's flow. |
| 78 | + |
| 79 | +const machine = new Sep24StatusMachine(); |
| 80 | +const { status } = await transfer.transaction(id, token); |
| 81 | +machine.transitionTo(status); // throws InvalidSep24TransitionError on an illegal jump |
| 82 | +``` |
| 83 | + |
| 84 | +`Sep24StatusMachine` tracks one transaction's lifecycle and rejects transitions the spec doesn't allow (e.g. leaving a terminal status), so a buggy poll loop can't silently mark a refunded deposit as completed. Re-applying the same status is a no-op, since anchors commonly re-report an unchanged status on every poll. |
| 85 | + |
| 86 | +## SEP-31 - cross-border payments |
| 87 | + |
| 88 | +```ts |
| 89 | +import { Sep31Client } from "@orbital-stellar/anchor-sdk"; |
| 90 | + |
| 91 | +const sep31 = new Sep31Client(toml.DIRECT_PAYMENT_SERVER!); |
| 92 | +const info = await sep31.info(); |
| 93 | + |
| 94 | +const { id, stellar_account_id, stellar_memo } = await sep31.initiateTransaction( |
| 95 | + { asset_code: "USDC", receiver_id: "..." }, |
| 96 | + token, |
| 97 | +); |
| 98 | +// Pay stellar_account_id with memo stellar_memo, then poll: |
| 99 | +const tx = await sep31.pollStatus(id, token); |
| 100 | +``` |
| 101 | + |
| 102 | +`sep31.sep12` is a bound `Sep12Client` for the same anchor, for when a SEP-31 flow needs sender/receiver KYC. `initiateTransaction` throws `MissingFieldsError` or `CustomerInfoNeededError` when the anchor needs more information before it will proceed. |
| 103 | + |
| 104 | +## Normalizing anchor events |
| 105 | + |
| 106 | +```ts |
| 107 | +import { normalizeAnchorEvent } from "@orbital-stellar/anchor-sdk"; |
| 108 | + |
| 109 | +const event = normalizeAnchorEvent(sep24Transaction); |
| 110 | +// event.type is one of the `anchor.*` lifecycle events from @orbital-stellar/pulse-core |
| 111 | +``` |
| 112 | + |
| 113 | +Maps a SEP-24 or SEP-31 transaction onto the `anchor.*` taxonomy in `@orbital-stellar/pulse-core`. The anchor's own status is always preserved verbatim in `protocolStatus` - the normalized `type` is a convenience layer, never a replacement, so a compliance consumer can still see exactly what the anchor said. `settlementTxHash` is only ever a hash the anchor actually published; it is `null` rather than guessed when the anchor doesn't expose one. |
| 114 | + |
| 115 | +## License |
| 116 | + |
| 117 | +MIT, see [LICENSE](./LICENSE). |
0 commit comments