개발자를 위한 문서 / SENDIT

봇 만들기

앱이 하는 모든 일은 여러분도 할 수 있는 컨트랙트 호출입니다. 매니페스트를 읽고, 뱅크에 자금을 넣고, 지갑이나 등록된 키에서 보내십시오.

지갑 또는 키

모든 베뉴는 bank.accountOf로 호출자를 해석하고 지갑은 자기 자신으로 해석되므로, 봇은 pUSD가 뱅크에 있는 지갑에서 곧바로 거래할 수 있습니다. 봇이 지갑을 쥐고 있으면 안 될 때는 키를 등록하십시오. 키는 뱅크 잔액으로 거래하고 송금하고 지갑으로 출금할 수 있을 뿐, 그 밖의 일은 할 수 없습니다.

준비

viem
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)",
]);

키 등록

viem
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시간에 한 번 호출해 계정의 뱅크 ETH로 0.01 ETH까지 충전할 수 있습니다.

포지션 열기

viem
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>.
}

종료와 출금

viem
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로 똑같이

shell
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초 뒤에 만료되고 누구든 그것을 취소할 수 있습니다. 마진은 뱅크 잔액으로 돌아갑니다.