const sleep = (s) => new Promise((r) => setTimeout(r, s * 1e3));
const api = async (method, path, body) => {
const r = await fetch(`https://api.sand.etherfuse.com${path}`, {
method,
headers: { Authorization: process.env.ETHERFUSE_API_KEY, "Content-Type": "application/json" },
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
const text = await r.text();
return text ? JSON.parse(text) : {};
};
const org = `/organization/${process.env.ORGANIZATION_ID}`;
const hex = (buf) => Buffer.from(buf).toString("hex");
async function approve(orderId, authorization, privateKey) {
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
privateKey,
new TextEncoder().encode(authorization.method.message),
);
// POST /organization/{organizationId}/order/{id}/authorization
await api("POST", `${org}/order/${orderId}/authorization`, {
id: authorization.id,
signature: hex(signature),
});
}
async function embeddedOnramp() {
const { publicKey, privateKey } = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign", "verify"],
);
const signerPublicKey = hex(await crypto.subtle.exportKey("raw", publicKey));
// POST /organization/{organizationId}/payment_account: we create the embedded wallet
const wallet = await api("POST", `${org}/payment_account`, {
kind: "embeddedWallet",
label: "Embedded wallet",
accountDetails: { kind: "signerPublicKey", chain: "stellar", signerPublicKey },
});
console.log("wallet account", wallet.id);
// POST /organization/{organizationId}/quote: MXN from the bank account into the wallet
const quote = await api("POST", `${org}/quote`, {
source: { type: "stored", id: process.env.BANK_ACCOUNT_ID },
destination: { type: "stored", id: wallet.id },
sourceAmount: "100",
sourceAsset: "MXN",
destinationAsset: process.env.TARGET_ASSET,
});
// POST /organization/{organizationId}/order: the order id is the quoteId
const order = await api("POST", `${org}/order`, { quoteId: quote.quoteId });
// POST /organization/{organizationId}/order/{id}/fiat_received: sandbox only
await api("POST", `${org}/order/${order.id}/fiat_received`, { amount: quote.sourceAmount });
// GET /organization/{organizationId}/order/{id}: in production, subscribe to orderUpdated instead of polling
let approvedId = null;
while (true) {
const current = await api("GET", `${org}/order/${order.id}`);
if (current.status === "completed") {
console.log("completed", current.destination);
return;
}
if (["failed", "canceled", "refunded"].includes(current.status)) {
throw new Error(`order ${current.status}: ${JSON.stringify(current.failure ?? {})}`);
}
if (current.status === "claimable" && current.authorization?.id !== approvedId) {
// POST /organization/{organizationId}/order/{id}/claim: no body
const claim = await api("POST", `${org}/order/${order.id}/claim`);
const { authorization } = await api("GET", `${org}/order/${order.id}`);
if (authorization?.id === claim.authorizationId) {
try {
await approve(order.id, authorization, privateKey);
approvedId = authorization.id;
} catch (e) {
// 409 authorization-expired: we replaced it, so the next pass signs the new one
if (!String(e.message).includes("authorization-expired")) throw e;
}
}
}
await sleep(15);
}
}
embeddedOnramp().catch(console.error);