Быстрый старт
Этот гайд проводит от регистрации до первого sweep в тестнете Sepolia. Вы зарегистрируете горячий кошелёк (котёл),
подпишете авторизацию EIP-7702 для тестового депозитного адреса, сметёте с него тестовый USDT, не отправляя на адрес
газ, и получите вебхук. В BSC testnet всё так же, только chain_id — 97.
Понадобятся:
- немного Sepolia ETH из крана — пополнить газ-кошелёк и выпустить тестовые токены;
curlиjq, а также Foundry радиcast;- для примеров на Go и TypeScript — Go 1.25+ или Node.js 20+ и SDK;
- адрес, которым вы управляете, — сюда придут средства, это и есть горячий кошелёк. В тестнете подойдёт любой новый адрес.
1. Аккаунт и тестовый ключ
Заголовок раздела «1. Аккаунт и тестовый ключ»- Зарегистрируйтесь в кабинете и подтвердите почту. Организация сразу получает аккаунт в тестнетах.
- Откройте API-ключи и создайте ключ. Скопируйте секрет сразу: он показывается один раз. Тестовые ключи
начинаются с
ptn_test_. - Храните ключ на бэкенде. Для команд ниже экспортируйте его вместе с адресом API тестнетов:
export PORTUNA_API_URL=https://api-testnet.portuna.ioexport PORTUNA_API_KEY=ptn_test_…Проверьте, что всё работает:
curl "$PORTUNA_API_URL/v1/balance" -H "Authorization: Bearer $PORTUNA_API_KEY"{ "gas_wallet": "0xf0259b04f5D336E624A84780d1B365fAFc3f5A85", "chains": [ { "chain_id": 97, "name": "bsc-testnet", "native": "BNB", "balance": "0", "available": "0", … }, { "chain_id": 11155111, "name": "sepolia", "native": "ETH", "balance": "0", "available": "0", … } ]}2. Пополните газ-кошелёк
Заголовок раздела «2. Пополните газ-кошелёк»gas_wallet — ваш газ-кошелёк: адрес, созданный для вас, один и тот же во всех сетях. С него Portuna платит газ
ваших sweep, так что депозитным адресам газ не нужен вообще. Пришлите на него тестовые монеты обычным переводом:
- Sepolia ETH — из Google Cloud Web3 Faucet или Sepolia PoW Faucet;
- tBNB в BSC testnet — из крана BNB Chain.
0,02 ETH с запасом хватит на развёртывание ваших контрактов и множество тестовых sweep. Когда перевод попадёт в блок,
available в GET /v1/balance покажет его в wei.
3. Зарегистрируйте горячий кошелёк
Заголовок раздела «3. Зарегистрируйте горячий кошелёк»Зарегистрируйте адрес, на который придут средства. Portuna развернёт с вашего газ-кошелька его делегат — контракт, который будут авторизовать депозитные адреса. Делегат умеет отправлять средства только в этот горячий кошелёк.
export HOT_WALLET=0x… # адрес, которым вы управляетеcurl -X POST "$PORTUNA_API_URL/v1/hot-wallets" \ -H "Authorization: Bearer $PORTUNA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"chain_id\": 11155111, \"address\": \"$HOT_WALLET\"}"{ "chain_id": 11155111, "address": "0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769", "delegate": "0xE60fdf70a794C76094f86b990a821D55e3096AA5", "status": "pending", "created_at": "2026-10-10T09:14:03Z"}Через несколько блоков статус станет active. Проверьте его и сохраните адрес делегата:
curl "$PORTUNA_API_URL/v1/hot-wallets" -H "Authorization: Bearer $PORTUNA_API_KEY" \ | jq '.hot_wallets[] | {chain_id, address, delegate, status}'export DELEGATE=0x… # "delegate" из ответаhotWallet := common.HexToAddress(os.Getenv("HOT_WALLET"))hot, err := client.CreateHotWallet(ctx, sepolia, hotWallet)if err != nil { log.Fatal(err)}// The delegate is deployed within a few blocks; the hot_wallet.active webhook event tells the same.for hot.Status == portuna.HotWalletPending { time.Sleep(5 * time.Second) if hot, err = findHotWallet(ctx, client, hotWallet); err != nil { log.Fatal(err) }}if hot.Status != portuna.HotWalletActive { log.Fatalf("the delegate was not deployed: %s", hot.Error)}fmt.Println("delegate:", hot.Delegate.Hex())Клиент и функция findHotWallet — в полной программе.
const hotWallet = env('HOT_WALLET') as Addresslet hot: HotWallet = await client.createHotWallet({ chain_id: chainId, address: hotWallet })// The delegate is deployed within a few blocks; the hot_wallet.active webhook event tells the same.while (hot.status === 'pending') { await sleep(5000) const all = await client.hotWallets() hot = all.find((h) => h.chain_id === chainId && isAddressEqual(h.address, hotWallet)) ?? hot}if (hot.status !== 'active') throw new Error(`the delegate was not deployed: ${hot.error}`)console.log('delegate:', hot.delegate)Если статус failed, причина — в поле error, чаще всего это газ-кошелёк без газа. Пополните его и отправьте тот же
запрос ещё раз: развёртывание начнётся заново.
4. Проверьте делегат
Заголовок раздела «4. Проверьте делегат»Прежде чем депозитный адрес подпишет что-либо для делегата, убедитесь, что это открытый код контракта, собранный для
вашего горячего кошелька и газ-кошелька. Команда verify-delegate из SDK делает это за один запуск; в каталоге Go SDK:
go run ./cmd/verify-delegate -rpc https://ethereum-sepolia-rpc.publicnode.com \ -gas-wallet <gas_wallet> -hot-wallet "$HOT_WALLET" -expect-delegate "$DELEGATE"Каждая строка должна начинаться с PASS. Подробности и ручная проверка — в гайде
Проверка делегата.
5. Подготовьте тестовый депозитный адрес
Заголовок раздела «5. Подготовьте тестовый депозитный адрес»Создайте новый ключ для тестового депозитного адреса. Это только тестовый ключ: настоящие ключи депозитных адресов остаются в вашей кастоди.
cast wallet newexport DEPOSIT=0x… # "Address" из выводаexport DEPOSIT_KEY=0x… # "Private key" из выводаexport RPC_URL=https://ethereum-sepolia-rpc.publicnode.comВыпустите на него тестовый USDT. Тестовый токен тестнетов — 0x5B74f75040584b133Ff1EB67eEe646B0da26e39C, один адрес в
Sepolia и BSC testnet — позволяет выпускать его кому угодно. Платите за выпуск с любого пополненного аккаунта в
Sepolia, только не с самого депозитного адреса: тогда на нём не будет ETH, а nonce останется 0:
cast send 0x5B74f75040584b133Ff1EB67eEe646B0da26e39C "mint(address,uint256)" "$DEPOSIT" 100000000 \ --rpc-url "$RPC_URL" --interactive100000000 — это 100 USDT: суммы в API — целые числа в минимальных единицах токена, а у USDT 6 знаков после запятой.
6. Подпишите авторизацию
Заголовок раздела «6. Подпишите авторизацию»Ключ депозитного адреса подписывает авторизацию EIP-7702: «в сети 11155111 выполнять код $DELEGATE» — при
текущем nonce адреса. Здесь подписывает тестовый ключ; в бою тот же 32-байтный хэш подписывает ваш HSM, KMS или MPC,
см. Подпись авторизаций.
С cast из Foundry и jq, сразу в формате API:
export NONCE=$(cast nonce "$DEPOSIT" --rpc-url "$RPC_URL") # 0 у нового адресаSIGNED=$(cast wallet sign-auth "$DELEGATE" --private-key "$DEPOSIT_KEY" --nonce "$NONCE" --chain 11155111)AUTH=$(cast from-rlp "$SIGNED" | jq -c --argjson nonce "$NONCE" \ '{chain_id: 11155111, address: .[1], nonce: $nonce, y_parity: (if .[3] == "0x" then 0 else 1 end), r: .[4], s: .[5]}')echo "$AUTH"// A test key in memory stands in for your custody here. An HSM, KMS or MPC signs the same digest raw, and// AuthorizationFromSignature takes its DER or r‖s signature as it is.key, err := crypto.HexToECDSA(strings.TrimPrefix(os.Getenv("DEPOSIT_KEY"), "0x"))if err != nil { log.Fatal(err)}deposit := crypto.PubkeyToAddress(key.PublicKey)
eth, err := ethclient.DialContext(ctx, os.Getenv("RPC_URL"))if err != nil { log.Fatal(err)}nonce, err := eth.NonceAt(ctx, deposit, nil) // 0 for an address that never sent a transactionif err != nil { log.Fatal(err)}digest, err := portuna.AuthorizationDigest(sepolia, hot.Delegate, nonce)if err != nil { log.Fatal(err)}sig, err := crypto.Sign(digest[:], key)if err != nil { log.Fatal(err)}auth, err := portuna.AuthorizationFromSignature(sepolia, hot.Delegate, nonce, deposit, sig)if err != nil { log.Fatal(err)}// A test key in memory stands in for your custody here. An HSM, KMS or MPC signs the same digest raw, and// authorizationFromSignature takes its DER or r‖s signature as it is.const privateKey = env('DEPOSIT_KEY') as Hexconst deposit = privateKeyToAccount(privateKey).address
const rpc = createPublicClient({ chain: sepolia, transport: http(env('RPC_URL')) })const nonce = await rpc.getTransactionCount({ address: deposit }) // 0 for an address that never sent a transactionconst digest = authorizationDigest({ chainId, address: hot.delegate, nonce })const signature = await sign({ hash: digest, privateKey, to: 'hex' })const authorization = await authorizationFromSignature({ chainId, address: hot.delegate, nonce, deposit, signature })7. Первый sweep
Заголовок раздела «7. Первый sweep»Запросите sweep всего баланса USDT на адресе — вместе с авторизацией:
curl -X POST "$PORTUNA_API_URL/v1/sweeps" \ -H "Authorization: Bearer $PORTUNA_API_KEY" \ -H "Content-Type: application/json" \ -d "$(jq -n --arg hot "$HOT_WALLET" --arg account "$DEPOSIT" --argjson auth "$AUTH" \ '{external_id: "quickstart-1", chain_id: 11155111, hot_wallet: $hot, account: $account, token: "USDT", authorization: $auth}')"Ответ — 202 с заданием в статусе queued. Следите за ним по id:
curl "$PORTUNA_API_URL/v1/sweeps/<id>" -H "Authorization: Bearer $PORTUNA_API_KEY"sweep, _, err := client.CreateSweep(ctx, portuna.SweepRequest{ ExternalID: "quickstart-" + deposit.Hex(), // repeating the request returns this same sweep ChainID: sepolia, HotWallet: hotWallet, Account: deposit, Token: "USDT", // the whole balance: no Amount Authorization: &auth,})if err != nil { log.Fatal(err)}for sweep.Status == portuna.SweepQueued || sweep.Status == portuna.SweepProcessing { time.Sleep(5 * time.Second) if sweep, err = client.Sweep(ctx, sweep.ID); err != nil { log.Fatal(err) }}fmt.Println(sweep.Status, sweep.SweptAmount, sweep.Reason, sweep.TxHash)let { sweep } = await client.createSweep({ external_id: `quickstart-${deposit}`, // repeating the request returns this same sweep chain_id: chainId, hot_wallet: hotWallet, account: deposit, token: 'USDT', // the whole balance: no amount authorization,})while (sweep.status === 'queued' || sweep.status === 'processing') { await sleep(5000) sweep = await client.sweep(sweep.id)}console.log(sweep.status, sweep.swept_amount ?? sweep.reason, sweep.tx_hash)Через минуту-другую задание станет swept: транзакция в блоке и набрала подтверждения.
{ "id": "3d0c5d0e-6b7f-4a5e-9a51-3c8f0b2e7d14", "external_id": "quickstart-1", "chain_id": 11155111, "hot_wallet": "0x2Cd50979a8A33e8CA37DA85F8517fb148a67b769", "account": "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23", "token": "USDT", "token_address": "0x5B74f75040584b133Ff1EB67eEe646B0da26e39C", "status": "swept", "attempts": 1, "swept_amount": "100000000", "tx_hash": "0x08deabdb72703741f2a02fc9eb22a93f74b0411156530e33da5237f4ef305704", "kind": "first_new", "gas": 270245, "gas_cost": "324294000000000", "fee": "0", "classic_gas": 237233, "rate_bps": 7000, "created_at": "2026-10-10T09:20:41Z", "updated_at": "2026-10-10T09:21:52Z"}Откройте tx_hash в Sepolia Etherscan: одна транзакция с вашего газ-кошелька
поставила делегат на депозитный адрес и перевела его USDT в горячий кошелёк. Теперь у адреса есть делегация —
0xef0100 и адрес делегата:
cast code "$DEPOSIT" --rpc-url "$RPC_URL"Комиссии за этот первый sweep не было. В Sepolia уже действуют правила газа Glamsterdam, и первый код на адресе, где лежат только токены, стоит примерно столько же газа, сколько классическое пополнение и перевод, — вы платите только газ. Экономия начинается с повторных sweep: см. Первый и повторный sweep.
8. Повторный sweep без авторизации
Заголовок раздела «8. Повторный sweep без авторизации»Выпустите на тот же адрес ещё тестового USDT и запросите новый sweep. На адресе уже стоит делегат, поэтому авторизация не нужна:
curl -X POST "$PORTUNA_API_URL/v1/sweeps" \ -H "Authorization: Bearer $PORTUNA_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"external_id\": \"quickstart-2\", \"chain_id\": 11155111, \"hot_wallet\": \"$HOT_WALLET\", \"account\": \"$DEPOSIT\", \"token\": \"USDT\"}"У этого задания kind: repeat. В батче из одного адреса оно стоит около 44 600 газа, в батче из 50 адресов — около
18 100 газа против 53 633 в классике.
9. Вебхуки
Заголовок раздела «9. Вебхуки»Задайте адрес, на который бэкенд будет получать события, и алерт на случай, когда газ-кошелёк опустеет:
curl -X PUT "$PORTUNA_API_URL/v1/webhook" \ -H "Authorization: Bearer $PORTUNA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://processing.example/hooks/sweeps", "low_balance": [{"chain_id": 11155111, "threshold": "10000000000000000"}]}'В ответе есть secret вида whsec_…: сохраните его на бэкенде, им проверяется каждая доставка. Адрес должен быть
публичным и по https; чтобы проверить приём со своей машины, поставьте перед ним HTTPS-туннель.
Проверяйте заголовок Portuna-Signature каждой доставки по сырому телу запроса и только потом обрабатывайте
событие:
http.HandleFunc("POST /hooks/sweeps", func(w http.ResponseWriter, r *http.Request) { // Verify the body exactly as it arrived, before parsing it. body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { http.Error(w, "cannot read the body", http.StatusBadRequest) return } event, err := portuna.ParseWebhook(secret, r.Header.Get(portuna.SignatureHeader), body) if err != nil { http.Error(w, "bad signature", http.StatusUnauthorized) return } // Delivery is at least once: an event may come again, with the same id. if handled(event.ID) { w.WriteHeader(http.StatusNoContent) return } switch event.Type { case portuna.EventSweepSwept, portuna.EventSweepFailed, portuna.EventSweepRejected: sweep, err := event.Sweep() if err != nil { http.Error(w, "bad event", http.StatusBadRequest) return } // Events may come out of order: the status in the data is the current one. log.Printf("sweep %s (%s): %s %s %s", sweep.ID, sweep.ExternalID, sweep.Status, sweep.SweptAmount, sweep.Reason) case portuna.EventBalanceLow: alert, err := event.BalanceLow() if err != nil { http.Error(w, "bad event", http.StatusBadRequest) return } log.Printf("top up the gas wallet on chain %d: %s wei left", alert.ChainID, alert.Balance) default: // Other and future event types: accept them. } markHandled(event.ID) w.WriteHeader(http.StatusNoContent)})function handle(event: Event): void { switch (event.type) { case 'sweep.swept': case 'sweep.failed': case 'sweep.rejected': // Events may come out of order: the status in the data is the current one. console.log(`sweep ${event.data.id} (${event.data.external_id}): ${event.data.status}`) break case 'balance.low': console.log(`top up the gas wallet on chain ${event.data.chain_id}: ${event.data.balance} wei left`) break default: // Other and future event types: accept them. }}
createServer((req, res) => { const chunks: Buffer[] = [] req.on('data', (chunk: Buffer) => chunks.push(chunk)) req.on('end', () => { // Verify the body exactly as it arrived, before parsing it. const body = Buffer.concat(chunks) let event: Event try { event = parseWebhookSync(secret, String(req.headers[signatureHeader.toLowerCase()] ?? ''), body) } catch { res.writeHead(401).end() return } // Delivery is at least once: an event may come again, with the same id. if (!handled.has(event.id)) { handle(event) handled.add(event.id) } res.writeHead(204).end() })}).listen(8080)# Verifies the Portuna-Signature header of a webhook delivery, with the standard library only.# The docs tests run it against the signature vectors the SDKs and the service share.import hashlibimport hmacimport reimport time
TOLERANCE = 300 # seconds between the signature time and now, either way
class SignatureError(Exception): pass
def verify_webhook(secret: str, header: str, body: bytes, now: float | None = None, tolerance: int = TOLERANCE) -> None: """Raises SignatureError unless the header signs body with secret within tolerance of now.
body must be the request body exactly as received, before any JSON parsing. """ ts, sig = None, None for part in header.split(","): key, _, value = part.partition("=") if key == "t": ts = value elif key == "v1": sig = value if ts is None or not re.fullmatch(r"[+-]?[0-9]+", ts) or not sig: raise SignatureError("malformed signature header") if abs((time.time() if now is None else now) - int(ts)) > tolerance: raise SignatureError("signature timestamp out of tolerance") expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig.encode(), expected.encode()): raise SignatureError("signature mismatch")POST /v1/webhook/test присылает событие webhook.test — проверить ваш приёмник, а GET /v1/webhook/deliveries
показывает, как прошли последние доставки. Подробнее — в гайде Вебхуки.
Полные программы
Заголовок раздела «Полные программы»Примеры на Go и TypeScript выше взяты из этих программ. Они собираются против SDK при каждой сборке документации, поэтому соответствуют текущему API.
// Quickstart on Sepolia: register a hot wallet, sign a deposit address's EIP-7702 authorization and sweep the// address's test USDT to the hot wallet.//// Environment: PORTUNA_API_URL, PORTUNA_API_KEY, HOT_WALLET, RPC_URL (a Sepolia RPC) and DEPOSIT_KEY, the private key of// a test deposit address. Never put a real deposit key in an environment variable: in production the key// stays in your HSM, KMS or MPC.package main
import ( "context" "fmt" "log" "os" "strings" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient"
portuna "github.com/Coddycoder/gasdelegate/sdk/go")
const sepolia = 11155111
func main() { ctx := context.Background()
client := portuna.NewClient(os.Getenv("PORTUNA_API_URL"), os.Getenv("PORTUNA_API_KEY"))
balance, err := client.Balance(ctx) if err != nil { log.Fatal(err) } fmt.Println("gas wallet:", balance.GasWallet.Hex()) for _, c := range balance.Chains { fmt.Printf("%s: %s wei available for gas\n", c.Name, c.Available) }
hotWallet := common.HexToAddress(os.Getenv("HOT_WALLET")) hot, err := client.CreateHotWallet(ctx, sepolia, hotWallet) if err != nil { log.Fatal(err) } // The delegate is deployed within a few blocks; the hot_wallet.active webhook event tells the same. for hot.Status == portuna.HotWalletPending { time.Sleep(5 * time.Second) if hot, err = findHotWallet(ctx, client, hotWallet); err != nil { log.Fatal(err) } } if hot.Status != portuna.HotWalletActive { log.Fatalf("the delegate was not deployed: %s", hot.Error) } fmt.Println("delegate:", hot.Delegate.Hex())
// A test key in memory stands in for your custody here. An HSM, KMS or MPC signs the same digest raw, and // AuthorizationFromSignature takes its DER or r‖s signature as it is. key, err := crypto.HexToECDSA(strings.TrimPrefix(os.Getenv("DEPOSIT_KEY"), "0x")) if err != nil { log.Fatal(err) } deposit := crypto.PubkeyToAddress(key.PublicKey)
eth, err := ethclient.DialContext(ctx, os.Getenv("RPC_URL")) if err != nil { log.Fatal(err) } nonce, err := eth.NonceAt(ctx, deposit, nil) // 0 for an address that never sent a transaction if err != nil { log.Fatal(err) } digest, err := portuna.AuthorizationDigest(sepolia, hot.Delegate, nonce) if err != nil { log.Fatal(err) } sig, err := crypto.Sign(digest[:], key) if err != nil { log.Fatal(err) } auth, err := portuna.AuthorizationFromSignature(sepolia, hot.Delegate, nonce, deposit, sig) if err != nil { log.Fatal(err) }
sweep, _, err := client.CreateSweep(ctx, portuna.SweepRequest{ ExternalID: "quickstart-" + deposit.Hex(), // repeating the request returns this same sweep ChainID: sepolia, HotWallet: hotWallet, Account: deposit, Token: "USDT", // the whole balance: no Amount Authorization: &auth, }) if err != nil { log.Fatal(err) } for sweep.Status == portuna.SweepQueued || sweep.Status == portuna.SweepProcessing { time.Sleep(5 * time.Second) if sweep, err = client.Sweep(ctx, sweep.ID); err != nil { log.Fatal(err) } } fmt.Println(sweep.Status, sweep.SweptAmount, sweep.Reason, sweep.TxHash)}
func findHotWallet(ctx context.Context, client *portuna.Client, address common.Address) (portuna.HotWallet, error) { list, err := client.HotWallets(ctx) if err != nil { return portuna.HotWallet{}, err } for _, h := range list { if h.ChainID == sepolia && h.Address == address { return h, nil } } return portuna.HotWallet{}, fmt.Errorf("hot wallet %s not found", address.Hex())}// Quickstart on Sepolia: register a hot wallet, sign a deposit address's EIP-7702 authorization and sweep the// address's test USDT to the hot wallet.//// Environment: PORTUNA_API_URL, PORTUNA_API_KEY, HOT_WALLET, RPC_URL (a Sepolia RPC) and DEPOSIT_KEY, the private key of a// test deposit address. Never put a real deposit key in an environment variable: in production the key stays// in your HSM, KMS or MPC.import { Client, type HotWallet, authorizationDigest, authorizationFromSignature } from '@portuna/sdk'import { type Address, type Hex, createPublicClient, http, isAddressEqual } from 'viem'import { privateKeyToAccount, sign } from 'viem/accounts'import { sepolia } from 'viem/chains'
const chainId = 11155111const env = (name: string): string => { const value = process.env[name] if (!value) throw new Error(`set ${name}`) return value}const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const client = new Client({ baseUrl: env('PORTUNA_API_URL'), apiKey: env('PORTUNA_API_KEY') })
const balance = await client.balance()console.log('gas wallet:', balance.gas_wallet)for (const c of balance.chains) console.log(`${c.name}: ${c.available} wei available for gas`)
const hotWallet = env('HOT_WALLET') as Addresslet hot: HotWallet = await client.createHotWallet({ chain_id: chainId, address: hotWallet })// The delegate is deployed within a few blocks; the hot_wallet.active webhook event tells the same.while (hot.status === 'pending') { await sleep(5000) const all = await client.hotWallets() hot = all.find((h) => h.chain_id === chainId && isAddressEqual(h.address, hotWallet)) ?? hot}if (hot.status !== 'active') throw new Error(`the delegate was not deployed: ${hot.error}`)console.log('delegate:', hot.delegate)
// A test key in memory stands in for your custody here. An HSM, KMS or MPC signs the same digest raw, and// authorizationFromSignature takes its DER or r‖s signature as it is.const privateKey = env('DEPOSIT_KEY') as Hexconst deposit = privateKeyToAccount(privateKey).address
const rpc = createPublicClient({ chain: sepolia, transport: http(env('RPC_URL')) })const nonce = await rpc.getTransactionCount({ address: deposit }) // 0 for an address that never sent a transactionconst digest = authorizationDigest({ chainId, address: hot.delegate, nonce })const signature = await sign({ hash: digest, privateKey, to: 'hex' })const authorization = await authorizationFromSignature({ chainId, address: hot.delegate, nonce, deposit, signature })
let { sweep } = await client.createSweep({ external_id: `quickstart-${deposit}`, // repeating the request returns this same sweep chain_id: chainId, hot_wallet: hotWallet, account: deposit, token: 'USDT', // the whole balance: no amount authorization,})while (sweep.status === 'queued' || sweep.status === 'processing') { await sleep(5000) sweep = await client.sweep(sweep.id)}console.log(sweep.status, sweep.swept_amount ?? sweep.reason, sweep.tx_hash)Что дальше
Заголовок раздела «Что дальше»- Как устроено: контракты и что каждому из них позволено.
- Подпись авторизаций в HSM, KMS и MPC.
- Рецепты: сколько сметать, батчи и нативная монета.
- Выход в мейннет: что подготовить к боевому запуску.