-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
263 lines (191 loc) · 11.3 KB
/
index.js
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// TonWeb is JavaScript SDK (Web and NodeJS) for TON
const TonWeb = require("tonweb");
// For calculations in the blockchain, we use BigNumber (BN.js). https://github.com/indutny/bn.js
// Don't use regular {Number} for coins, etc., it has not enough size and there will be loss of accuracy.
const BN = TonWeb.utils.BN;
// Blockchain does not operate with fractional numbers like `0.5`.
// `toNano` function converts TON to nanoton - smallest unit.
// 1 TON = 10^9 nanoton; 1 nanoton = 0.000000001 TON;
// So 0.5 TON is 500000000 nanoton
const toNano = TonWeb.utils.toNano;
const init = async () => {
const providerUrl = 'https://testnet.toncenter.com/api/v2/jsonRPC'; // TON HTTP API url. Use this url for testnet
const apiKey = ''; // Obtain your API key in https://t.me/tontestnetapibot
const tonweb = new TonWeb(new TonWeb.HttpProvider(providerUrl, {apiKey})); // Initialize TON SDK
//----------------------------------------------------------------------
// PARTIES
// The payment channel is established between two participants A and B.
// Each has own secret key, which he does not reveal to the other.
// New secret key can be generated by `tonweb.utils.newSeed()`
tonweb.utils.newSeed(); // Uint8Array
const seedA = TonWeb.utils.base64ToBytes('vt58J2v6FaSuXFGcyGtqT5elpVxcZ+I1zgu/GUfA5uY='); // A's private (secret) key
const keyPairA = tonweb.utils.keyPairFromSeed(seedA); // Obtain key pair (public key and private key)
const seedB = TonWeb.utils.base64ToBytes('at58J2v6FaSuXFGcyGtqT5elpVxcZ+I1zgu/GUfA5uY='); // B's private (secret) key
const keyPairB = tonweb.utils.keyPairFromSeed(seedB); // Obtain key pair (public key and private key)
// if you are new to cryptography then the public key is like a login, and the private key is like a password.
// Login can be shared with anyone, password cannot be shared with anyone.
// With a key pair, you can create a wallet.
// Note that this is just an object, we are not deploying anything to the blockchain yet.
// Transfer some amount of test coins to this wallet address (from your wallet app).
// To check you can use blockchain explorer https://testnet.tonscan.org/address/<WALLET_ADDRESS>
const walletA = tonweb.wallet.create({
publicKey: keyPairA.publicKey
});
const walletAddressA = await walletA.getAddress(); // address of this wallet in blockchain
console.log('walletAddressA = ', walletAddressA.toString(true, true, true));
const walletB = tonweb.wallet.create({
publicKey: keyPairB.publicKey
});
const walletAddressB = await walletB.getAddress(); // address of this wallet in blockchain
console.log('walletAddressB = ', walletAddressB.toString(true, true, true));
//----------------------------------------------------------------------
// PREPARE PAYMENT CHANNEL
// The parties agree on the configuration of the payment channel.
// They share information about the payment channel ID, their public keys, their wallet addresses for withdrawing coins, initial balances.
// They share this information off-chain, for example via a websocket.
const channelInitState = {
balanceA: toNano('1'), // A's initial balance in Toncoins. Next A will need to make a top-up for this amount
balanceB: toNano('2'), // B's initial balance in Toncoins. Next B will need to make a top-up for this amount
seqnoA: new BN(0), // initially 0
seqnoB: new BN(0) // initially 0
};
const channelConfig = {
channelId: new BN(124), // Channel ID, for each new channel there must be a new ID
addressA: walletAddressA, // A's funds will be withdrawn to this wallet address after the channel is closed
addressB: walletAddressB, // B's funds will be withdrawn to this wallet address after the channel is closed
initBalanceA: channelInitState.balanceA,
initBalanceB: channelInitState.balanceB
}
// Each on their side creates a payment channel object with this configuration
const channelA = tonweb.payments.createChannel({
...channelConfig,
isA: true,
myKeyPair: keyPairA,
hisPublicKey: keyPairB.publicKey,
});
const channelAddress = await channelA.getAddress(); // address of this payment channel smart-contract in blockchain
console.log('channelAddress=', channelAddress.toString(true, true, true));
const channelB = tonweb.payments.createChannel({
...channelConfig,
isA: false,
myKeyPair: keyPairB,
hisPublicKey: keyPairA.publicKey,
});
if ((await channelB.getAddress()).toString() !== channelAddress.toString()) {
throw new Error('Channels address not same');
}
// Interaction with the smart contract of the payment channel is carried out by sending messages from the wallet to it.
// So let's create helpers for such sends.
const fromWalletA = channelA.fromWallet({
wallet: walletA,
secretKey: keyPairA.secretKey
});
const fromWalletB = channelB.fromWallet({
wallet: walletB,
secretKey: keyPairB.secretKey
});
//----------------------------------------------------------------------
// NOTE:
// Further we will interact with the blockchain.
// After each interaction with the blockchain, we need to wait for execution. In the TON blockchain, this is usually about 5 seconds.
// In this example, the interaction code happens right after each other - that won't work.
// To study the example, you can put a `return` after each send.
// In a real application, you will need to check that the smart contract of the channel has changed
// (for example, by calling its get-method and checking the `state`) and only then do the following action.
//----------------------------------------------------------------------
// DEPLOY PAYMENT CHANNEL FROM WALLET A
// Wallet A must have a balance.
// 0.05 TON is the amount to execute this transaction on the blockchain. The unused portion will be returned.
// After this action, a smart contract of our payment channel will be created in the blockchain.
await fromWalletA.deploy().send(toNano('0.05'));
// To check you can use blockchain explorer https://testnet.tonscan.org/address/<CHANNEL_ADDRESS>
// We can also call get methods on the channel (it's free) to get its current data.
console.log(await channelA.getChannelState());
const data = await channelA.getData();
console.log('balanceA = ', data.balanceA.toString())
console.log('balanceB = ', data.balanceB.toString())
// TOP UP
// Now each parties must send their initial balance from the wallet to the channel contract.
await fromWalletA
.topUp({coinsA: channelInitState.balanceA, coinsB: new BN(0)})
.send(channelInitState.balanceA.add(toNano('0.05'))); // +0.05 TON to network fees
await fromWalletB
.topUp({coinsA: new BN(0), coinsB: channelInitState.balanceB})
.send(channelInitState.balanceB.add(toNano('0.05'))); // +0.05 TON to network fees
// to check, call the get method - the balances should change
// INIT
// After everyone has done top-up, we can initialize the channel from any wallet
await fromWalletA.init(channelInitState).send(toNano('0.05'));
// to check, call the get method - `state` should change to `TonWeb.payments.PaymentChannel.STATE_OPEN`
//----------------------------------------------------------------------
// FIRST OFFCHAIN TRANSFER - A sends 0.1 TON to B
// A creates new state - subtracts 0.1 from A's balance, adds 0.1 to B's balance, increases A's seqno by 1
const channelState1 = {
balanceA: toNano('0.9'),
balanceB: toNano('2.1'),
seqnoA: new BN(1),
seqnoB: new BN(0)
};
// A signs this state and send signed state to B (e.g. via websocket)
const signatureA1 = await channelA.signState(channelState1);
// B checks that the state is changed according to the rules, signs this state, send signed state to A (e.g. via websocket)
if (!(await channelB.verifyState(channelState1, signatureA1))) {
throw new Error('Invalid A signature');
}
const signatureB1 = await channelB.signState(channelState1);
//----------------------------------------------------------------------
// SECOND OFFCHAIN TRANSFER - A sends 0.2 TON to B
// A creates new state - subtracts 0.2 from A's balance, adds 0.2 to B's balance, increases A's seqno by 1
const channelState2 = {
balanceA: toNano('0.7'),
balanceB: toNano('2.3'),
seqnoA: new BN(2),
seqnoB: new BN(0)
};
// A signs this state and send signed state to B (e.g. via websocket)
const signatureA2 = await channelA.signState(channelState2);
// B checks that the state is changed according to the rules, signs this state, send signed state to A (e.g. via websocket)
if (!(await channelB.verifyState(channelState2, signatureA2))) {
throw new Error('Invalid A signature');
}
const signatureB2 = await channelB.signState(channelState2);
//----------------------------------------------------------------------
// THIRD OFFCHAIN TRANSFER - B sends 1.1 TON TO A
// B creates new state - subtracts 1.1 from B's balance, adds 1.1 to A's balance, increases B's seqno by 1
const channelState3 = {
balanceA: toNano('1.8'),
balanceB: toNano('1.2'),
seqnoA: new BN(2),
seqnoB: new BN(1)
};
// B signs this state and send signed state to A (e.g. via websocket)
const signatureB3 = await channelB.signState(channelState3);
// A checks that the state is changed according to the rules, signs this state, send signed state to B (e.g. via websocket)
if (!(await channelA.verifyState(channelState3, signatureB3))) {
throw new Error('Invalid B signature');
}
const signatureA3 = await channelA.signState(channelState3);
//----------------------------------------------------------------------
// So they can do this endlessly.
// Note that a party can make its transfers (from itself to another) asynchronously without waiting for the action of the other side.
// Party must increase its seqno by 1 for each of its transfers and indicate the last seqno and balance of the other party that it knows.
//----------------------------------------------------------------------
// CLOSE PAYMENT CHANNEL
// The parties decide to end the transfer session.
// If one of the parties disagrees or is not available, then the payment channel can be emergency terminated using the last signed state.
// That is why the parties send signed states to each other off-chain.
// But in our case, they do it by mutual agreement.
// First B signs closing message with last state, B sends it to A (e.g. via websocket)
const signatureCloseB = await channelB.signClose(channelState3);
// A verifies and signs this closing message and include B's signature
// A sends closing message to blockchain, payments channel smart contract
// Payment channel smart contract will send funds to participants according to the balances of the sent state.
if (!(await channelA.verifyClose(channelState3, signatureCloseB))) {
throw new Error('Invalid B signature');
}
await fromWalletA.close({
...channelState3,
hisSignature: signatureCloseB
}).send(toNano('0.05'));
}
init();