-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
223 lines (190 loc) · 8.14 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import {Api, JsonRpc} from "eosjs";
import fetch from 'node-fetch';
import {IAtomicMarketConfig, IDelphiOracleToken, ISale} from "./types";
import {JsSignatureProvider} from "eosjs/dist/eosjs-jssig";
import {stringToSymbol} from "eosjs/dist/eosjs-serialize";
import {exactNumberShift, splitEosioToken} from "./utils";
// CONFIG
const RPC_ENDPOINT = 'https://wax.pink.gg'
const MARKET_CONTRACT = 'atomicmarket';
const ALLOWED_SYMBOLS = ['WAX'];
// TODO use private key and proper account permissions
const BUYER_PRIVATE_KEYS: string[] = [];
const BUYER_AUTHORIZATION = {actor: 'account', permission: 'perm'}
// CODE STARTS HERE
const rpc = new JsonRpc(RPC_ENDPOINT, {fetch});
const api = new Api({rpc, signatureProvider: new JsSignatureProvider(BUYER_PRIVATE_KEYS)})
async function getConfig(): Promise<IAtomicMarketConfig> {
// this fetches the atomicmarket config table from the blockchain to get supported symbols and pairs
const resp = await rpc.get_table_rows({
json: true, code: MARKET_CONTRACT, scope: MARKET_CONTRACT, table: 'config', limit: 1
});
if (!Array.isArray(resp.rows) || resp.rows.length !== 1) {
throw new Error('No config found for market contract');
}
return resp.rows[0];
}
async function getDelphiPairs(config: IAtomicMarketConfig): Promise<IDelphiOracleToken[]> {
const result: IDelphiOracleToken[] = [];
for (const pair of config.supported_symbol_pairs) {
const resp = await rpc.get_table_rows({
json: true,
code: config.delphioracle_account,
scope: config.delphioracle_account,
table: 'pairs',
limit: 1,
lower_bound: pair.delphi_pair_name,
upper_bound: pair.delphi_pair_name
});
if (!Array.isArray(resp.rows) || resp.rows.length !== 1) {
throw new Error('Delphi pair not found');
}
result.push({
...pair,
median_precision: resp.rows[0].quoted_precision,
base_precision: stringToSymbol(resp.rows[0].base_symbol).precision,
quote_precision: stringToSymbol(resp.rows[0].quote_symbol).precision
});
}
return result;
}
async function getSale(config: IAtomicMarketConfig, pairs: IDelphiOracleToken[], saleID: string): Promise<ISale> {
// This fetches information about the sale from the blockchain
const sales = await rpc.get_table_rows({
json: true,
code: MARKET_CONTRACT,
scope: MARKET_CONTRACT,
table: 'sales',
lower_bound: saleID,
upper_bound: saleID
});
if (!Array.isArray(sales.rows) || sales.rows.length !== 1) {
throw new Error('Sale not found');
}
const sale = sales.rows[0];
const listingPrice = splitEosioToken(sale.listing_price);
const settlementSymbol = stringToSymbol(sale.settlement_symbol);
if (!ALLOWED_SYMBOLS.includes(settlementSymbol.name)) {
throw new Error('Symbol not supported');
}
const token = config.supported_tokens.find(row => stringToSymbol(row.token_symbol).name === settlementSymbol.name);
if (!token) {
throw new Error('Symbol not found');
}
const price = {
token_symbol: settlementSymbol.name,
token_precision: settlementSymbol.precision,
token_contract: token.token_contract,
amount: listingPrice.amount,
intended_delphi_median: '0'
}
// When listing symbol is different to settlement symbol we need additional conversion to get the price in the token we want
if (listingPrice.token_symbol !== settlementSymbol.name) {
const pair = pairs.find(row =>
stringToSymbol(row.listing_symbol).name === listingPrice.token_symbol
&& stringToSymbol(row.settlement_symbol).name === settlementSymbol.name
);
if (!pair) {
throw new Error('Symbol pair not supported');
}
const datapoints = await rpc.get_table_rows({
json: true,
limit: 1,
code: config.delphioracle_account,
scope: pair.delphi_pair_name,
table: 'datapoints',
index_position: 3,
key_type: 'i64',
reverse: true
});
if (!Array.isArray(datapoints.rows) || datapoints.rows.length !== 1) {
throw new Error('No delphi datapoint found');
}
if (pair.invert_delphi_pair) {
price.amount = (+price.amount * datapoints.rows[0].median * Math.pow(10, pair.quote_precision - pair.base_precision - pair.median_precision)).toFixed(0)
} else {
price.amount = (+price.amount / datapoints.rows[0].median * Math.pow(10, pair.median_precision + pair.base_precision - pair.quote_precision)).toFixed(0)
}
price.intended_delphi_median = String(datapoints.rows[0].median);
}
return {
sale_id: sale.sale_id,
seller: sale.seller,
asset_ids: sale.asset_ids,
price,
listing_price: sale.listing_price,
settlement_symbol: sale.settlement_symbol
}
}
async function purchaseSale(config: IAtomicMarketConfig, sale: ISale, receiver: string): Promise<void> {
// We only need to send 1 transaction that includes multiple actions
await api.transact({
actions: [
// This is needed to make sure that we are buying the correct listing in case a fork happened when we checked the last time
{
account: MARKET_CONTRACT,
name: 'assertsale',
authorization: [BUYER_AUTHORIZATION],
data: {
sale_id: sale.sale_id,
asset_ids_to_assert: sale.asset_ids,
listing_price_to_assert: sale.listing_price,
settlement_symbol_to_assert: sale.settlement_symbol
}
},
// Here we deposit the tokens to the smart contract we need for the purchase
{
account: sale.price.token_contract,
name: 'transfer',
authorization: [BUYER_AUTHORIZATION],
data: {
from: BUYER_AUTHORIZATION.actor,
to: MARKET_CONTRACT,
quantity: `${exactNumberShift(sale.price.amount, sale.price.token_precision)} ${sale.price.token_symbol}`,
memo: 'deposit'
}
},
// Here we purchase the NFT, so we have it in the buyer account
{
account: MARKET_CONTRACT,
name: 'purchasesale',
authorization: [BUYER_AUTHORIZATION],
data: {
buyer: BUYER_AUTHORIZATION.actor,
sale_id: sale.sale_id,
intended_delphi_median: sale.price.intended_delphi_median,
taker_marketplace: '.'
}
},
// Here we transfer the NFTs to the real buyer in the same transaction
{
account: config.atomicassets_account,
name: 'transfer',
authorization: [BUYER_AUTHORIZATION],
data: {
from: BUYER_AUTHORIZATION.actor,
to: receiver,
asset_ids: sale.asset_ids,
memo: `AtomicMarket Purchased Sale - ID # ${sale.sale_id}`
}
}
]
}, {useLastIrreversible: true, expireSeconds: 30});
}
// EXAMPLE
(async () => {
// This is the user that is buying with a credit card
const user = 'testbuyer';
// the getConfig() and getDelphiPairs() result can be cached and reused between purchases
const config = await getConfig();
const pairs = await getDelphiPairs(config);
// this get details about a sale by sale_id
const sale = await getSale(config, pairs, '68987754');
console.log('sale', sale);
// TODO Buy required token amount to execute purchase
console.log('----------------')
console.log(`${user} needs to buy ${exactNumberShift(sale.price.amount, sale.price.token_precision)} ${sale.price.token_symbol}`)
console.log('----------------')
// execute purchase when payment was successful
await purchaseSale(config, sale, user);
})();