Skip to content
LogoLogo

Issuer platform from the SDK

The Arrekta platform services sit behind the gateway, and both SDKs cover them with the same functions (camelCase in TypeScript, snake_case in Python):

ServicePathWhat it does
Gateway/gateway/v1/*API keys (each key's id is your principal), usage, signed webhooks
Wallet service/wallet/v1/*m-of-n wallets whose co-signers keep their own keys; policy engine; PST signature collection
Issuer service/issuer/v1/*investor registry and signed claims; transfer rules by co-signature; airdrops, dividends, governed controls, attestations, public signed log
Indexer/esplora/*asset supply history and cap tables at any height
Faucet/faucet/v1/*test ARRK behind a proof-of-work captcha

Arrekta Studio (apps/studio) is built only on these calls; read its code for a complete application. Design: Studio.

1. A 2-of-3 treasury

Each co-signer derives keys on its own device and registers them in a batch signed by its identity key, so the service cannot substitute keys.

import { createGatewayClient, createServiceWallet, cosignerIdentityHex, registerWalletKeys, walletKeyBatch, nextWalletAddress } from 'arrekta-sdk'
 
const gw = createGatewayClient({ url: 'http://127.0.0.1:3000', apiKey: 'arrk_…' })
const w = await createServiceWallet(gw, {
  label: 'treasury', m: 2,
  cosigners: [
    { name: 'issuer', identity_key: cosignerIdentityHex(deviceSeed), principal: myKeyId },
    { name: 'co-signer', identity_key: cosignerIdentityHex(cosignerSeed) },
    { name: 'backup', identity_key: cosignerIdentityHex(backupSeed) },
  ],
})
await registerWalletKeys(gw, { wallet: w.id, batch: walletKeyBatch({ root: deviceSeed, walletId: w.id, cosigner: 0 }) })
// … the other co-signers register theirs; addresses appear once all three have
const { address } = await nextWalletAddress(gw, { wallet: w.id })
from arrekta_sdk import GatewayClient, cosigner_identity_hex, wallet_key_batch
 
gw = GatewayClient("http://127.0.0.1:3000", api_key="arrk_…")
w = gw.create_service_wallet("treasury", [
    {"name": "issuer", "identity_key": cosigner_identity_hex(device_seed), "principal": my_key_id},
    {"name": "co-signer", "identity_key": cosigner_identity_hex(cosigner_seed)},
    {"name": "backup", "identity_key": cosigner_identity_hex(backup_seed)},
], m=2)
gw.register_wallet_keys(w["id"], wallet_key_batch(device_seed, w["id"], 0))

2. Issue a coin from the treasury

Build an unsigned PST from the wallet's coins and import it: the wallet service applies the policy, then each device signs. The coin and its authority and control tokens stay in the treasury. Fees assume 2-of-3 inputs (multisigInputSize).

import { buildServiceWalletPst, createIssuance, createWalletTransfer, getWalletUnspents, issuedAssetId, metadataHash,
         pstFromBase64, pstSummary, pstToBase64, signPstWithSeed, submitWalletSignatures, verifyPstAttestation, getWalletAttestationKey, AssetFlags } from 'arrekta-sdk'
 
const doc = { schema: 'arrk-ams-1', ticker: 'BOND', name: 'Green Bond', issuer: { name: 'Example Capital' }, controls_policy: '…' }
const issuance = createIssuance({ ticker: 'BOND', name: 'Green Bond', amount: 1_000_000n, flags: AssetFlags.FREEZABLE | AssetFlags.CLAWBACK, metadataHash: metadataHash(doc) })
const unspents = await getWalletUnspents(gw, { wallet: w.id })
const { pst, tx } = buildServiceWalletPst({ unspents, network: 'regtest', payments: [], change: address, feeRate: 2000, issuances: [issuance] })
const t = await createWalletTransfer(gw, { wallet: w.id, sequenceId: 'issue-bond', pst })
 
const key = await getWalletAttestationKey(gw)
if (!verifyPstAttestation(t.pst!, t.attestation!.signature, key.public_key)) throw new Error('tampered PST')
console.log(pstSummary(pstFromBase64(t.pst!), deviceSeed)) // what leaves, change, fee
await submitWalletSignatures(gw, { transfer: t.id, pst: pstToBase64(signPstWithSeed(pstFromBase64(t.pst!), deviceSeed)) })

Python: build_service_wallet_pst(unspents, network=…, change=…, fee_per_kb=…, issuances=[…]), then create_wallet_transfer, verify_pst_attestation, submit_wallet_signatures.

3. Investors, claims and holders

import { createIssuer, addClaimIssuer, createInvestor, addInvestorClaim, signIssuerClaim, registerIssuerAsset, registerHolder, holderProof } from 'arrekta-sdk'
 
const issuer = await createIssuer(gw, { name: 'Example Capital', treasuryWallet: w.id, quorum: 1 })
await registerIssuerAsset(gw, { issuer: issuer.id, asset: assetHex, mode: 'restricted', rules: { receiver: [[{ claim: 'kyc' }]] }, metadata: doc })
const ci = await addClaimIssuer(gw, { issuer: issuer.id, name: 'KYC', publicKey: kycKeyHex })
const alice = await createInvestor(gw, { issuer: issuer.id, name: 'Alice' })
await addInvestorClaim(gw, { investor: alice.id, type: 'kyc', value: 'true', claimIssuer: ci.id,
  signature: signIssuerClaim(kycKey, { issuerId: issuer.id, investorId: alice.id, type: 'kyc', value: 'true' }) })
// the holder proves control of their key; the result is a managed 2-of-2 [holder, authorizer] address
const managed = await registerHolder(gw, { asset: assetHex, holder: holderProof(aliceKey, assetHex, alice.id) })

A holder moves a restricted coin by signing a PST from the managed address and asking authorizeTransfer for the authorizer's co-signature. The rules are checked first.

Show holders their address under your coin's own ticker: encodeCoinAddress(lock, ticker, network) (encode_coin_address) turns rarrk1… into <ticker>-r1… (<ticker>1… on mainnet), the same lock. Studio portals do this for every coin.

4. Airdrops, rewards and controls

TaskTypeScriptPython
allocate (optionally vesting until a height)createAssignmentcreate_assignment
pay vested allocations through the treasurydistribute → sign its payouts[].wallet_transferdistribute
reward run from a buried snapshotdeclareDividend, payDividenddeclare_dividend, pay_dividend
recompute a dividend tabledividendTableHashdividend_table_hash
freeze, unfreeze, clawback, rule changeproposeControl → approveControl → executeControlsame, snake_case
reserve attestationpublishAttestationpublish_attestation
public log, verifiedgetPublicAssetLog + verifyIssuerLogget_public_asset_log + verify_issuer_log
cap table at a record dategetAssetHolders({ asset, height })get_asset_holders(asset, height=…)

5. Sign in with Arrekta

Accounts without passwords: the user signs a short message with their key. The message is bound to your domain, so a signature for one site is useless on another.

import { signIn, verifySignIn } from 'arrekta-sdk'
 
// browser
const { message, signature } = signIn(accountKey, { domain: location.host, network: 'regtest', nonce, issuedAt, expiresAt })
// server: then check that the nonce is yours and unused
const req = verifySignIn({ message, signature, domain: 'studio.example', network: 'regtest' })
from arrekta_sdk import sign_in, verify_sign_in
message, signature = sign_in(key, domain="studio.example", network="regtest", nonce=nonce, issued_at=t0, expires_at=t1)
req = verify_sign_in(message, signature, domain="studio.example", network="regtest")

6. Faucet (testnet and devnet)

import { createClient, claimFaucet } from 'arrekta-sdk'
await claimFaucet(createClient({ url: 'http://127.0.0.1:28090' }), { address })
from arrekta_sdk import Client
Client("http://127.0.0.1:28090").claim_faucet(address)

The faucet's proof of work is solved locally (solveFaucetChallenge / solve_faucet_challenge).