Construa um bot de trading
Um bot de trading mínimo: um comerciante publica um anúncio de venda (OFS-2100), depois uma identidade de bot separada o descobre e abre uma reserva contra ele (OFS-2200). O bloqueio do escrow é automático ao solicitar — sem passo de confirmação separado.
Inicie primeiro um nó local:
cargo run -p openfiat-cli -- --rpc-bind-address 127.0.0.1:7080
Rust
Fonte completa: examples/trading_bot.rs
(cargo run --example trading_bot a partir de openfiat-sdks/rust).
let merchant = Keypair::generate();
let bot = Keypair::generate();
// Vinculado uma vez. A reserva abaixo tem de concordar com ele exatamente, e
// escrever o número duas vezes é como os dois se descolam.
let advertised_price = Amount::new(12_950, 2);
let create = AdvertisementCreate {
id: AdvertisementId::new("my-ad"),
merchant: peer_id(&merchant),
merchant_public_key: merchant.public_key(),
// Um endereço de mint, não um ticker — veja abaixo.
asset_mint: MintAddress::parse("C4rSGhdxWhSFQuFcAxQti1JvBxriwHJoHtJjfhs5p24Y")?,
direction: Direction::Sell,
fiat_currency: FiatCurrency::parse("KES")?,
// Denominado no ATIVO, não na moeda fiat acima.
min_trade: Amount::new(1_000, 2),
max_trade: Amount::new(50_000, 2),
initial_liquidity: Amount::new(200_000, 2),
pricing: PricingModel::Fixed { price: advertised_price },
payment_methods: vec!["M-Pesa".to_string()],
timestamp: Timestamp::now(),
};
let ad_id = client.send_advertisement_create(create, &merchant).await?;
// Um bot real chamaria em vez disso client.get_advertisements() com um filtro
// que descreve sua estratégia — a reserva só precisa do ID.
let request = ReservationRequest {
id: ReservationId::new("my-reservation"),
advertisement_id: ad_id,
requester: peer_id(&bot),
requester_public_key: bot.public_key(),
amount: Amount::new(5_000, 2),
// O preço que o bot está aceitando, assinado na requisição.
agreed_price: advertised_price,
agreed_mid: None,
timestamp: Timestamp::now(),
};
let reservation_id = client.send_reservation_request(request, &bot).await?;
let reservation = client.get_reservation(reservation_id.as_str()).await?.unwrap();
println!("{:?}", reservation.state); // EscrowLocked
Um anúncio nomeia um mint, não um ticker
asset_mint é um endereço de mint da Solana em base58. Um ticker é um rótulo que
o comerciante escolheu, e nada o amarra ao token que o escrow de fato moveria —
um anúncio poderia dizer «USDT» e liquidar em outra coisa, com cada camada
concordando que a negociação se completou, porque cada uma fez exatamente o que
lhe foi pedido.
Você nunca fornece um símbolo. O nó resolve um a partir do mint e o retorna ao
lado do registro como asset_symbol, que é null para um mint do qual ele não
tem nome. Mostre o endereço nesse caso; um endereço sem apelido é inútil e
verdadeiro, e um palpite é útil e falso.
Uma reserva fixa o preço com que concordou
agreed_price é assinado na requisição, e o nó confere que ele decorre dos
próprios termos do anúncio — recusando com PRICE_DISAGREEMENT caso contrário.
Isso é o que impede um comerciante de reprecificar entre a cotação que um bot leu
e o escrow que ele bloqueia.
Este anúncio é Fixed, então o preço acordado é simplesmente o que ele anuncia e
não há mid a registrar. Contra um Floating, um bot lê ambos do quote em
getAdvertisement e passa agreed_mid também — é o que permite ao nó
re-derivar o mesmo número a partir da mesma observação do oráculo em vez da sua
própria, que pode diferir.
getReservation é uma leitura aberta e não autenticada, então o que volta é a
visão redigida: o estado, o valor e o anúncio estão
ali, o solicitante não. Um bot que precisa de suas próprias reservas por completo
— incluindo os campos das partes — as lê com uma
prova de carteira.
TypeScript
Fonte completa: examples/trading_bot.ts
(pnpm tsx examples/trading_bot.ts a partir de openfiat-sdks/typescript).
const merchant = await generateKeypair();
const bot = await generateKeypair();
// Vinculado uma vez: a reserva abaixo deve concordar com ele exatamente.
const advertisedPrice = { base_units: 12_950, decimals: 2 };
const create: AdvertisementCreate = {
id: "my-ad",
merchant: toBytes(peerIdFromPublicKey(merchant.publicKey)),
merchant_public_key: toBytes(merchant.publicKey),
asset_mint: "C4rSGhdxWhSFQuFcAxQti1JvBxriwHJoHtJjfhs5p24Y",
direction: "Sell",
fiat_currency: "KES",
min_trade: { base_units: 1_000, decimals: 2 },
max_trade: { base_units: 50_000, decimals: 2 },
initial_liquidity: { base_units: 200_000, decimals: 2 },
pricing: { Fixed: { price: advertisedPrice } },
payment_methods: ["M-Pesa"],
timestamp: Date.now(),
};
const adId = await advertisements.sendAdvertisementCreate(client, create, merchant);
// Um bot real chamaria em vez disso advertisements.getAdvertisements(client, {…})
// com um filtro que descreve sua estratégia — a reserva só precisa do ID.
const request: ReservationRequest = {
id: "my-reservation",
advertisement_id: adId,
requester: toBytes(peerIdFromPublicKey(bot.publicKey)),
requester_public_key: toBytes(bot.publicKey),
amount: { base_units: 5_000, decimals: 2 },
agreed_price: advertisedPrice,
agreed_mid: null,
timestamp: Date.now(),
};
const reservationId = await reservations.sendReservationRequest(client, request, bot);
const reservation = await reservations.getReservation(client, reservationId);
console.log(reservation?.state); // "EscrowLocked"
Lendo o livro
getAdvertisements recebe um filtro e um cursor e responde com uma página:
{ advertisements, next_cursor }. Ele respondia com um arranjo nu de cada
anúncio da rede, a partir de uma chamada que não recebia parâmetros — uma
resposta crescendo sem limite sobre um livro que ninguém podia buscar.
Estreite na requisição. Filtrar uma página depois que ela chega não só desperdiça a transferência: o limite da página foi decidido sobre linhas que você depois descartou, então o cursor já não significa o que você pensa.
let cursor: string | null = null;
do {
const page = await advertisements.getAdvertisements(client, {
filter: { fiat_currency: "KES", direction: "Sell" },
page: { after: cursor, limit: 25 },
});
for (const ad of page.advertisements) {
// `quote` é uma união discriminada, e os três casos são três promessas
// distintas. `Fixed` se move só quando o comerciante assina um novo;
// `Floating` vale até `mid_expires_at` e pode se mover antes disso;
// `Unpriceable` significa que o anúncio existe e não tem preço agora.
// Ler `price` dele sem estreitar renderizaria um anúncio sem preço como
// gratuito.
switch (ad.quote.kind) {
case "Fixed":
consider(ad, ad.quote.price);
break;
case "Floating":
if (ad.quote.mid_expires_at > Date.now()) consider(ad, ad.quote.price);
break;
case "Unpriceable":
skip(ad, ad.quote.reason); // NoOracleData | StaleOracleData | PriceOutOfRange
break;
}
}
cursor = page.next_cursor;
} while (cursor !== null);
Passe next_cursor de volta tal e qual. Ele por acaso é um id de anúncio, e
ainda assim não é um a reconstruir a partir da última linha que você recebeu:
fazê-lo significa reimplementar a ordenação do nó, e um leitor cuja ordenação
discorda recebe algumas linhas duas vezes e outras nunca, sem nada que o
indique. Pare quando o cursor for null, não quando uma página estiver vazia —
uma página cheia não prova que outra existe, então o nó pode devolver um cursor
sem nada atrás.