Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra currency support #386

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/boc/BitString.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,33 @@ class BitString {
this.writeGrams(amount);
}

/**
* @param bits {number} VarUint bits
* @param amount {number | BN} value
* var_uint$_ {n:#} len:(#< n) value:(uint (len * 8)) = VarUInteger n;
*/
writeVarUint(bits, amount) {
const lengthBits = Math.ceil(Math.log2(bits));
if(amount == 0) {
this.writeUint(0, lengthBits);
} else {
if(amount < 0) {
throw RangeError("VarUint amount has to be unsigned!");
}
const bigAmount = new BN(amount);
const l = bigAmount.byteLength();
const bitLen = l * 8;
// If VarUint bits are, let's say 32, then lenght prefix is 5 bits.
// Max amount storable in 5 bits is 31, so max value length is 31 * 8
const maxValueLen = (bits - 1) * 8;
if(bitLen > maxValueLen) {
throw RangeError(`Value too long ${bitLen}/${maxValueLen}`);
}
this.writeUint(l, lengthBits);
this.writeUint(bigAmount, bitLen);
}
}

//addr_none$00 = MsgAddressExt;
//addr_std$10 anycast:(Maybe Anycast)
// workchain_id:int8 address:uint256 = MsgAddressInt;
Expand Down
2 changes: 1 addition & 1 deletion src/contract/highloadWallet/HighloadWalletContractV3.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class HighloadWalletContractV3 extends Contract {
/**
* @param secretKey {Uint8Array} nacl.KeyPair.secretKey
* @param address {Address | string}
* @param amount {BN | number} in nanotons
* @param amount {BN | number | CurrencyCollection} in nanotons
* @param queryId {HighloadQueryId}
* @param [payload] {string | Uint8Array | Cell}
* @param [sendMode] {number}
Expand Down
44 changes: 39 additions & 5 deletions src/contract/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {Cell} = require("../boc");
const {Address, bytesToBase64, bytesToHex, BN} = require("../utils");
const {Address, bytesToBase64, bytesToHex, BN, CurrencyCollection} = require("../utils");

class Contract {
/**
Expand Down Expand Up @@ -100,6 +100,28 @@ class Contract {
// currencies$_ grams:Grams other:ExtraCurrencyCollection
// = CurrencyCollection;

/**
* @param id {number} Extra currency id
* @param amount {number | BN} EC amount
*/

static createExtraCurrencyCollection(id, amount) {
const ecData = new Cell();

let maxSignedInt32 = (2 ** 31) - 1;
if(id > maxSignedInt32 || id < -maxSignedInt32) {
throw Error(`Extra currency id is out of 32 bit range: ${id}`);
}

// hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
ecData.bits.writeUint(0b10, 2); // tag
ecData.bits.writeUint(32, 6); // Label len
ecData.bits.writeUint(id, 32); // 32 bit label
ecData.bits.writeVarUint(32, amount); // Data

return ecData;
}

//int_msg_info$0 ihr_disabled:Bool bounce:Bool
//src:MsgAddressInt dest:MsgAddressInt
//value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
Expand All @@ -111,7 +133,7 @@ class Contract {
* @param bounce {null | boolean}
* @param bounced {boolean}
* @param src {Address | string}
* @param currencyCollection {null}
* @param currencyCollection {Cell | null}
* @param ihrFees {number | BN}
* @param fwdFees {number | BN}
* @param createdLt {number | BN}
Expand Down Expand Up @@ -142,7 +164,7 @@ class Contract {
message.bits.writeAddress(new Address(dest));
message.bits.writeGrams(gramValue);
if (currencyCollection) {
throw "Currency collections are not implemented yet";
message.refs.push(currencyCollection);
}
message.bits.writeBit(Boolean(currencyCollection));
message.bits.writeGrams(ihrFees);
Expand Down Expand Up @@ -173,7 +195,7 @@ class Contract {

/**
* @param address {Address | string}
* @param amount {BN} in nanotons
* @param amount {BN} | {CurrencyCollection} in nanotons
* @param payload {string | Uint8Array | Cell}
* @param stateInit? {Cell}
* @return {Cell}
Expand All @@ -193,7 +215,19 @@ class Contract {
}
}

const orderHeader = Contract.createInternalMessageHeader(new Address(address), new BN(amount));
let bnAmount;
let extraCell = null;

if(amount instanceof CurrencyCollection) {
bnAmount = amount.value;
extraCell = this.createExtraCurrencyCollection(amount.extra.id, amount.extra.value);
} else if(BN.isBN(amount)) {
bnAmount = amount;
} else {
bnAmount = new BN(amount);
}

const orderHeader = Contract.createInternalMessageHeader(new Address(address), bnAmount, true, null, false, null, extraCell);
const order = Contract.createCommonMsgInfo(orderHeader, stateInit, payloadCell);
return order;
}
Expand Down
4 changes: 2 additions & 2 deletions src/contract/wallet/WalletContract.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class WalletContract extends Contract {
/**
* @param secretKey {Uint8Array} nacl.KeyPair.secretKey
* @param address {Address | string}
* @param amount {BN | number} in nanotons
* @param amount {BN | number | CurrencyCollection} in nanotons
* @param seqno {number}
* @param payload? {string | Uint8Array | Cell}
* @param sendMode? {number}
Expand Down Expand Up @@ -202,7 +202,7 @@ class WalletContract extends Contract {
/**
* @param secretKey {Uint8Array} nacl.KeyPair.secretKey
* @param seqno {number}
* @param messages {[{toAddress: Address | string, amount: BN, payload?: string | Uint8Array | Cell, sendMode?: number, stateInit?: Cell }]} up to 4 messages
* @param messages {[{toAddress: Address | string, amount: BN | CurrencyCollection, payload?: string | Uint8Array | Cell, sendMode?: number, stateInit?: Cell }]} up to 4 messages
* @param dummySignature? {boolean}
* @param expireAt? {number}
* @return {Promise<{address: Address, signature: Uint8Array, message: Cell, cell: Cell, body: Cell, resultMessage: Cell}>}
Expand Down
34 changes: 34 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ class TonWeb {
async getTransactions(address, limit = 20, lt = undefined, txhash = undefined, to_lt = undefined) {
return this.provider.getTransactions(address.toString(), limit, lt, txhash, to_lt);
};
/*
* @param from {Address | string},
* @param to { Address | string},
* @param lt { BN | string},
* @param hash { string },
* @param maxTry?: { number },
* @param waitTime?: { number }
*/
async waitForTx (from, to, lt, hash, maxTry = 20, waitTime = 2000) {

let tryCount = 0;

let lastTx = {lt: new TonWeb.utils.BN(lt), hash};
let fromAddress = typeof from == 'string' ? new this.utils.Address(from) : from;
let toAddress = typeof to == 'string' ? new this.utils.Address(to) : to;

do {
await this.utils.waitSome();

const txs = await this.getTransactions(toAddress.toString(), 10, undefined, lastTx.hash, lastTx.lt.toString());
const newTxs = txs.filter(x => new this.utils.BN(x.transaction_id.lt).gt(lastTx.lt));
if(newTxs.length > 0) {
const highloadTx = newTxs.find(x => new Address(x.in_msg.source).toString(false) == fromAddress.toString(false));
if(highloadTx) {
return highloadTx;
}
lastTx = {lt: new TonWeb.utils.BN(newTxs[0].lt), hash: newTxs[0].hash};
}

if(++tryCount > maxTry) {
throw Error(`Sending request is not processed within ${Math.floor(waitTime / 1000) * maxTry} sec. Likely failed`);
}
} while(true);
}

/**
* @param address {Address | string}
Expand Down
89 changes: 76 additions & 13 deletions src/test-highload.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,99 @@
const TonWeb = require("./index");
const {HighloadWalletContractV3, HighloadQueryId} = TonWeb.HighloadWallets;
const {Address, toNano} = TonWeb.utils;
const init = async () => {

const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC', {apiKey: ''}));
const receiver = new Address('UQCdqXGvONLwOr3zCNX5FjapflorB6ZsOdcdfLrjsDLt3AF4');
const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC', {apiKey: ''}));

// Create v4 wallet
const seed = TonWeb.utils.hexToBytes('607cdaf518cd38050b536005bea2667d008d5dda1027f9549479f4a42ac315c4');

const keyPair = TonWeb.utils.nacl.sign.keyPair.fromSeed(seed);
const highloadWallet = new HighloadWalletContractV3(tonweb.provider, {
publicKey: keyPair.publicKey,
timeout: 60 * 60, // 1 hour
});

let queryId = new HighloadQueryId();

const seed = TonWeb.utils.hexToBytes('607cdaf518cd38050b536005bea2667d008d5dda1027f9549479f4a42ac315c4');

const keyPair = TonWeb.utils.nacl.sign.keyPair.fromSeed(seed);
console.log('wallet public key =', TonWeb.utils.bytesToHex(keyPair.publicKey));

const highloadWallet = new HighloadWalletContractV3(tonweb.provider, {
publicKey: keyPair.publicKey,
timeout: 60 * 60, // 1 hour
const ecTransfer = async () => {

console.log("Testing extra currency send");
// queryId = queryId.getNext();

const createAt = Math.floor(Date.now() / 1000) - 200;
console.log(createAt);

const stateBefore = await tonweb.provider.getAddressInfo(receiver.toString());
let lastTx = {lt: new TonWeb.utils.BN(stateBefore.last_transaction_id.lt), hash: stateBefore.last_transaction_id.hash};
const toSend = new TonWeb.utils.BN(1 * (10 ** 6));
let balanceBefore;
let ecFound = stateBefore.extra_currencies.find(x => (x.id == 100));
if(ecFound) {
balanceBefore = new TonWeb.utils.BN(ecFound.amount);
} else {
balanceBefore = new TonWeb.utils.BN(0);
}

const transfer = highloadWallet.methods.transfer({
secretKey: keyPair.secretKey,
queryId: queryId,
createdAt: createAt,
toAddress: receiver.toString(),
amount: new TonWeb.utils.CurrencyCollection(0, {id: 100, value: toSend}),
payload: 'Hello highload EC',
sendMode: 3,
needDeploy: queryId.getQueryId() === 0n
});


await transfer.send();

const depositTx = await tonweb.waitForTx(highloadWallet.address, receiver, lastTx.lt, lastTx.hash);

if(depositTx.in_msg.extra_currencies.find(c => c.id == 100 && new TonWeb.utils.BN(c.amount).eq(toSend)) == undefined) {
console.log(depositTx);
throw Error("Incoming message doesn't contain expected amount of ec");
}
const stateAfter = await tonweb.provider.getAddressInfo(receiver.toString());
ecFound = stateAfter.extra_currencies.find(x => (x.id == 100));
if(ecFound) {
if((new TonWeb.utils.BN(ecFound.amount)).eq(balanceBefore.add(toSend))) {
console.log(`${receiver.toString(true, true, true)} got ${toSend} EC`);
} else {
console.log(stateAfter);
throw Error(`Expected balance ${balanceBefore.add(toSend)} got ${ecFound.amount}`);
}
} else {
console.log(stateAfter);
throw Error("No extra currency with id 100 present on receiver address");
}
}

const init = async () => {

console.log('wallet public key =', TonWeb.utils.bytesToHex(keyPair.publicKey));

const highloadAddress = await highloadWallet.getAddress();

console.log('Highload-wallet address is ' + highloadAddress.toString(true, true, true));

let queryId = new HighloadQueryId();
queryId = queryId.getNext();
const stateBefore = await tonweb.provider.getAddressInfo(receiver.toString(true, true, true));
const ltBefore = stateBefore.last_transaction_id.lt;

do {
queryId = queryId.getNext();
} while(await highloadWallet.isProcessed(queryId, false));

const createAt = Math.floor(Date.now() / 1000) - 60;
const createAt = Math.floor(Date.now() / 1000) - 200;
console.log(createAt);

const transfer = highloadWallet.methods.transfer({
secretKey: keyPair.secretKey,
queryId: queryId,
createdAt: createAt,
toAddress: new Address('UQCdqXGvONLwOr3zCNX5FjapflorB6ZsOdcdfLrjsDLt3AF4'),
toAddress: receiver,
amount: toNano('0.01'), // 0.01 TON
payload: 'Hello',
sendMode: 3,
Expand All @@ -48,6 +110,7 @@ const init = async () => {
console.log('getTimeout', await highloadWallet.getTimeout());
console.log('getPublicKey', await highloadWallet.getPublicKey());

await ecTransfer();
}

init();
60 changes: 59 additions & 1 deletion src/test-wallet4.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function init() {

// wallet seqno get method

const seqno = (await wallet.methods.seqno().call()) || 0;
let seqno = (await wallet.methods.seqno().call()) || 0;
console.log('wallet seqno = ', seqno);

// Simple transfer
Expand All @@ -48,6 +48,63 @@ async function init() {
);
}

const ecTransfer = async () => {
const stateBefore = await tonweb.provider.getAddressInfo(BENEFICIARY);
const toSend = new TonWeb.utils.BN(1 * (10 ** 6));
const prevSeqno = seqno;
let balanceBefore;
let ecFound = stateBefore.extra_currencies.find(x => (x.id == 100));
if(ecFound) {
balanceBefore = new TonWeb.utils.BN(ecFound.amount);
} else {
balanceBefore = new TonWeb.utils.BN(0);
}
console.log(balanceBefore.toString());
await TonWeb.utils.waitSome();
console.log(
'Extra currency transfer',
await wallet.methods.transfer({
secretKey: keyPair.secretKey,
toAddress: BENEFICIARY,
amount: new TonWeb.utils.CurrencyCollection(0, {id: 100, value: toSend}),
seqno: seqno || 0,
payload: 'Hello EC',
sendMode: 3
}).send()
);

let retryCount = 0;
do {
await TonWeb.utils.waitSome();
seqno = await wallet.methods.seqno().call();
if(retryCount++ > 10) {
throw Error("Seqno didn't update withing 20 sec. Sending likely failed");
}
} while(prevSeqno == seqno);

console.log("Message sent successfully!");

const depositTx = await tonweb.waitForTx(wallet.address, BENEFICIARY, stateBefore.last_transaction_id.lt, stateBefore.last_transaction_id.hash);
if(depositTx.in_msg.extra_currencies.find(c => c.id == 100 && new TonWeb.utils.BN(c.amount).eq(toSend)) == undefined) {
throw Error("Deposit tx not found!");
} else {
console.log("Deposit tx found!");
}
const stateAfter = await tonweb.provider.getAddressInfo(BENEFICIARY);
ecFound = stateAfter.extra_currencies.find(x => (x.id == 100));
if(ecFound) {
if((new TonWeb.utils.BN(ecFound.amount)).eq(balanceBefore.add(toSend))) {
console.log(`${BENEFICIARY} got ${toSend} EC`);
} else {
console.log(stateAfter);
throw Error(`Expected balance ${balanceBefore.add(toSend)} got ${ecFound.amount}`);
}
} else {
console.log(stateAfter);
throw Error("No extra currency with id 100 present on receiver address");
}
}

// Create subscription

const subscription = new SubscriptionContract(tonweb.provider, {
Expand Down Expand Up @@ -140,6 +197,7 @@ async function init() {
//

// await simpleTransfer();
await ecTransfer();
// await deployAndInstallPlugin();
// await installPlugin();
// await removePlugin();
Expand Down
Loading