Construire un bot de trading
Un bot de trading minimal : un commerçant publie une annonce de vente (OFS-2100), puis une identité de bot séparée la découvre et ouvre une réservation contre elle (OFS-2200). Le verrouillage de l’escrow est automatique à la demande — pas d’étape de confirmation séparée.
Démarrez d’abord un nœud local :
cargo run -p openfiat-cli -- --rpc-bind-address 127.0.0.1:7080
Rust
Source complète : examples/trading_bot.rs
(cargo run --example trading_bot depuis openfiat-sdks/rust).
let merchant = Keypair::generate();
let bot = Keypair::generate();
// Lié une fois. La réservation ci-dessous doit y correspondre exactement, et
// écrire le nombre deux fois est la façon dont les deux dérivent.
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(),
// Une adresse de mint, pas un ticker — voir ci-dessous.
asset_mint: MintAddress::parse("C4rSGhdxWhSFQuFcAxQti1JvBxriwHJoHtJjfhs5p24Y")?,
direction: Direction::Sell,
fiat_currency: FiatCurrency::parse("KES")?,
// Libellé dans l’ACTIF, pas dans la monnaie fiat ci-dessus.
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?;
// Un vrai bot appellerait plutôt client.get_advertisements() avec un filtre
// décrivant sa stratégie — la réservation n’a besoin que de l’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),
// Le prix que le bot accepte, signé dans la requête.
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
Une annonce nomme un mint, pas un ticker
asset_mint est une adresse de mint Solana en base58. Un ticker est une étiquette
que le commerçant a choisie, et rien ne l’attache au token que l’escrow
déplacerait réellement — une annonce pourrait dire « USDT » et se régler en autre
chose, chaque couche s’accordant sur le fait que l’échange s’est achevé, parce
que chacune a fait exactement ce qu’on lui a demandé.
Vous ne fournissez jamais un symbole. Le nœud en résout un à partir du mint et le
renvoie à côté de l’enregistrement comme asset_symbol, qui vaut null pour un
mint dont il n’a pas de nom. Affichez l’adresse dans ce cas ; une adresse sans
surnom est inutile et vraie, et une supposition est utile et fausse.
Une réservation épingle le prix convenu
agreed_price est signé dans la requête, et le nœud vérifie qu’il découle des
propres termes de l’annonce — refusant par PRICE_DISAGREEMENT sinon. C’est ce
qui empêche un commerçant de reprixer entre la cotation qu’un bot a lue et
l’escrow qu’il verrouille.
Cette annonce est Fixed, donc le prix convenu est simplement ce qu’elle annonce
et il n’y a pas de mid à enregistrer. Contre une Floating, un bot lit les deux
depuis le quote sur getAdvertisement et passe aussi agreed_mid — c’est ce
qui permet au nœud de re-dériver le même nombre à partir de la même observation
d’oracle plutôt que de la sienne, qui peut différer.
getReservation est une lecture ouverte et non authentifiée, donc ce qui revient
est la vue rédigée : l’état, le montant et l’annonce
sont là, le demandeur non. Un bot qui a besoin de ses propres réservations en
entier — y compris les champs des parties — les lit plutôt avec une
preuve de portefeuille.
TypeScript
Source complète : examples/trading_bot.ts
(pnpm tsx examples/trading_bot.ts depuis openfiat-sdks/typescript).
const merchant = await generateKeypair();
const bot = await generateKeypair();
// Lié une fois : la réservation ci-dessous doit y correspondre exactement.
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);
// Un vrai bot appellerait plutôt advertisements.getAdvertisements(client, {…})
// avec un filtre décrivant sa stratégie — la réservation n’a besoin que de l’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"
Lire le carnet
getAdvertisements prend un filtre et un curseur et répond par une page :
{ advertisements, next_cursor }. Il répondait par un tableau nu de chaque
annonce du réseau, à partir d’un appel qui ne prenait aucun paramètre — une
réponse croissant sans limite sur un carnet que personne ne pouvait chercher.
Restreignez dans la requête. Filtrer une page après son arrivée ne gaspille pas seulement le transfert : la limite de page a été décidée sur des lignes que vous avez ensuite jetées, donc le curseur ne signifie plus ce que vous pensez.
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` est une union discriminée, et les trois cas sont trois promesses
// différentes. `Fixed` ne bouge que quand le commerçant en signe une
// nouvelle ; `Floating` est valide jusqu’à `mid_expires_at` et peut bouger
// avant ; `Unpriceable` signifie que l’annonce existe et n’a pas de prix
// pour l’instant. Lire `price` dessus sans restreindre rendrait une annonce
// non tarifable comme gratuite.
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);
Repassez next_cursor tel quel. Il se trouve être un id d’annonce, et il
n’est pourtant pas à reconstruire depuis la dernière ligne reçue : le faire
signifie réimplémenter l’ordonnancement du nœud, et un lecteur dont
l’ordonnancement diverge se voit remettre certaines lignes deux fois et d’autres
jamais, sans que rien ne le signale. Arrêtez quand le curseur vaut null, pas
quand une page est vide — une page pleine ne prouve pas qu’une autre existe, donc
le nœud peut renvoyer un curseur sans rien derrière.