開発者へ / SENDIT
ボットを作る
アプリが行うことはすべて、あなたにも呼べるコントラクト呼び出しです。マニフェストを読み、バンクに入金し、ウォレットか登録済みのキーから送信します。
ウォレットかキーか
すべてのベニューは呼び出し元を bank.accountOf で解決し、ウォレットは自分自身に解決されるので、ボットは pUSD がバンクにあるウォレットから直接取引できます。ボットにウォレットを持たせたくない場合はキーを登録してください。キーはバンク残高で取引と送金を行い、ウォレットへ引き出せますが、それ以外は何もできません。
セットアップ
import { createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEther, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const pickle = defineChain({
id: 78270,
name: "Pickle testnet",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.picklechain.xyz"] } },
});
const ORIGIN = "https://sendit.picklechain.xyz";
const m = await fetch(`${ORIGIN}/op-deployment/l2-perps.json`).then((r) => r.json());
const client = createPublicClient({ chain: pickle, transport: http() });
const bankAbi = parseAbi([
"function deposit(uint256 pusdAmount, uint256 gasToKey, address key, bytes keyProof) payable",
"function setKey(address key, bytes keyProof)",
"function keyDigest(address account, address key) view returns (bytes32)",
"function accountOf(address sender) view returns (address)",
"function balanceOf(address account, address asset) view returns (uint256)",
"function withdraw(address asset, uint256 amount)",
]);
const perpsAbi = parseAbi([
"struct OpenArgs { bytes32 feedId; bool isLong; uint256 margin; uint16 leverage; uint256 tp; uint256 sl; }",
"function open(OpenArgs a) returns (uint256 id)",
"function close(uint256 id)",
"function isImmediate(bytes32 feedId) view returns (bool)",
"function whyNotOpen(address trader, OpenArgs a) view returns (string)",
"function whyNotClose(address caller, uint256 id) view returns (string)",
"event PositionOpened(uint256 indexed positionId, address indexed trader, bytes32 indexed feedId, (bool isLong, uint16 leverage, uint256 margin, uint256 units, uint256 cost, uint256 price, uint256 fee, int256 fundingIndex) info)",
]);
const ordersAbi = parseAbi([
"struct OpenRequest { bytes32 feedId; bool isLong; uint256 margin; uint16 leverage; uint256 acceptablePrice; uint256 tp; uint256 sl; }",
"function requestOpen(OpenRequest r) returns (uint256 id)",
"function whyNotRequestOpen(address trader, OpenRequest r) view returns (string)",
]);キーを登録する
const wallet = createWalletClient({ chain: pickle, transport: http(), account: privateKeyToAccount(process.env.WALLET_KEY) });
const key = privateKeyToAccount(process.env.TRADING_KEY); // any key you generated and keep
const account = wallet.account.address;
// The key's consent, under the epochs in force: this ADDS the key.
const digest = await client.readContract({ address: m.bank, abi: bankAbi, functionName: "keyDigest", args: [account, key.address] });
const proof = await key.signMessage({ message: { raw: digest } });
// One wallet transaction: register the key and send it 0.01 ETH for gas.
// (Add pUSD from the wallet as the first argument; it needs no approve.)
const gas = parseEther("0.01");
await wallet.writeContract({
address: m.bank, abi: bankAbi, functionName: "deposit",
args: [0n, gas, key.address, proof], value: gas,
});
// From here the key acts for the account.
const keyWallet = createWalletClient({ chain: pickle, transport: http(), account: key });キーは自分のガスを自分で支払います。0.002 ETH を下回ると、キーはバンクの refuel() を 1 時間に 1 回呼び、アカウントのバンクの ETH から 0.01 ETH まで補充できます。
ポジションを開く
const { markets } = await fetch(`${ORIGIN}/api/perp/markets`).then((r) => r.json());
const btc = markets.find((x) => x.symbol === "BTC/USD" && x.enabled && !x.legacy);
const args = {
feedId: btc.feedId,
isLong: true,
margin: parseUnits("10", 6), // $10 of pUSD from the bank balance
leverage: 5,
tp: 0n, // oracle units (1e8), 0 for none
sl: 0n,
};
if (await client.readContract({ address: m.perps, abi: perpsAbi, functionName: "isImmediate", args: [args.feedId] })) {
// Ask first: revert reasons do not reach a client on this chain.
const why = await client.readContract({ address: m.perps, abi: perpsAbi, functionName: "whyNotOpen", args: [account, args] });
if (why) throw new Error(why);
const hash = await keyWallet.writeContract({ address: m.perps, abi: perpsAbi, functionName: "open", args: [args] });
await client.waitForTransactionReceipt({ hash });
} else {
const req = { ...args, acceptablePrice: 0n }; // or a bound in oracle units
const why = await client.readContract({ address: m.perpOrders, abi: ordersAbi, functionName: "whyNotRequestOpen", args: [account, req] });
if (why) throw new Error(why);
await keyWallet.writeContract({ address: m.perpOrders, abi: ordersAbi, functionName: "requestOpen", args: [req] });
// A keeper fills it at the next price round; follow it on /api/perp/requests?trader=<account>.
}決済と引き出し
const { positions } = await fetch(`${ORIGIN}/api/perp/positions?trader=${account}`).then((r) => r.json());
for (const p of positions.filter((x) => !x.legacy)) {
const why = await client.readContract({ address: m.perps, abi: perpsAbi, functionName: "whyNotClose", args: [key.address, BigInt(p.positionId)] });
if (!why) await keyWallet.writeContract({ address: m.perps, abi: perpsAbi, functionName: "close", args: [BigInt(p.positionId)] });
}
// Withdraw always pays the account's wallet, even when the key signs.
const bal = await client.readContract({ address: m.bank, abi: bankAbi, functionName: "balanceOf", args: [account, m.pusd] });
await keyWallet.writeContract({ address: m.bank, abi: bankAbi, functionName: "withdraw", args: [m.pusd, bal] });cast で同じことを
RPC=https://rpc.picklechain.xyz ORIGIN=https://sendit.picklechain.xyz M=$(curl -s $ORIGIN/op-deployment/l2-perps.json) BANK=$(echo "$M" | jq -r .bank) PERPS=$(echo "$M" | jq -r .perps) FEED=$(curl -s $ORIGIN/api/perp/markets | jq -r '.markets[] | select(.symbol=="ETH/USD" and .legacy==false) | .feedId') ME=$(cast wallet address --private-key $PK) # Optional: move 100 pUSD from the wallet into the bank (no approve needed). cast send $BANK "deposit(uint256,uint256,address,bytes)" 100000000 0 $(cast address-zero) 0x \ --private-key $PK --rpc-url $RPC # Wallet-signed works too: the wallet is its own account. cast call $PERPS "whyNotOpen(address,(bytes32,bool,uint256,uint16,uint256,uint256))(string)" \ $ME "($FEED,true,10000000,5,0,0)" --rpc-url $RPC cast send $PERPS "open((bytes32,bool,uint256,uint16,uint256,uint256))" \ "($FEED,true,10000000,5,0,0)" --private-key $PK --rpc-url $RPC
監視
ポジション、リクエスト、約定は dex-api にあります。dex-api はコントラクトのログをインデックスするもので、少し遅れることがあります。お金が動くことについては、エンジンを読んでください:getPosition、positionState、isLiquidatable、そして PositionOpened と PositionClosed イベントです。これらのイベントの trader は常にアカウントです。
キーパーが止まった場合
2 段階の約定、トリガー、コピーのミラーリングは SENDIT のキーパーが行います。キーパーが止まると、リクエストは 120 秒後に失効し、誰でもキャンセルできます。あなたの証拠金はバンク残高に戻ります。