tonweb
Version:
TonWeb - JavaScript API for TON blockchain
242 lines (199 loc) • 9.18 kB
JavaScript
const TonWeb = require("./index");
const BN = TonWeb.utils.BN;
const Address = TonWeb.utils.Address;
/**
* Storage for storing block numbers that we have already processed.
*
* Dumb in-memory block numbers storage implementation
* Just an example of what it should do
*/
class BlocksStorageImpl {
masterchainBlocks = {}; // mcBlockNumber {number} -> isProcessed {boolean}
shardchainBlocks = {}; // shardId {string} + shardBlockNumber {number} -> isProcessed {boolean}
constructor() {
}
/**
* @private
* Insert new UNprocessed shardchain block numbers
* Block number (shardId + shardBlockNumber) should be IGNORED if it is already in the storage
* @param shardBlockNumbers {[{shardId: string, shardBlockNumber: number}]}
*/
async insertShardBlocks(shardBlockNumbers) {
for (const {shardId, shardBlockNumber} of shardBlockNumbers) {
if (this.shardchainBlocks[shardId + '_' + shardBlockNumber] !== undefined) continue;
// INSERT INTO shardchainBlocks VALUES (shardId, shardBlockNumber, FALSE)
console.log('insert shard ' + shardId + ' ' + shardBlockNumber);
this.shardchainBlocks[shardId + '_' + shardBlockNumber] = false;
}
}
/**
* Insert new processed masterchain block number & new UNprocessed shardchains blocks numbers
* Must be in single DB transaction
* @param mcBlockNumber {number}
* @param shardBlockNumbers {[{shardId: string, shardBlockNumber: number}]}
*/
async insertBlocks(mcBlockNumber, shardBlockNumbers) {
console.log('mc processed ' + mcBlockNumber);
// INSERT INTO masterchainBlocks VALUES (blockNumber, TRUE)
if (this.masterchainBlocks[mcBlockNumber] !== undefined) throw new Error('mc already exists ' + mcBlockNumber);
this.masterchainBlocks[mcBlockNumber] = true;
await this.insertShardBlocks(shardBlockNumbers);
}
/**
* Get last processed masterchain block number
* @return {Promise<number | undefined>}
*/
async getLastMasterchainBlockNumber() {
// SELECT MAX(blockNumber) FROM masterchainBlocks
const blockNumbers = Object.keys(this.masterchainBlocks)
.map(x => Number(x))
.sort((a, b) => b - a);
return blockNumbers[0];
}
/**
* Set that this shardchain block number processed & insert new UNprocessed shardchains blocks numbers
* Must be in single DB transaction
* @param shardId {string}
* @param shardBlockNumber {number}
* @param prevShardBlocks {[{shardId: string, shardBlockNumber: number}]}
*/
async setBlockProcessed(shardId, shardBlockNumber, prevShardBlocks) {
console.log('shard processed ' + shardId + ' ' + shardBlockNumber);
// UPDATE shardchainBlocks SET processed = TRUE WHERE shardId = ? && shardBlockNumber = ?
if (this.shardchainBlocks[shardId + '_' + shardBlockNumber] === undefined) throw new Error('shard not exists ' + shardId + '_' + shardBlockNumber);
this.shardchainBlocks[shardId + '_' + shardBlockNumber] = true;
await this.insertShardBlocks(prevShardBlocks);
}
/**
* Get any unprocesed shard block number (order is not important)
* @return {Promise<{shardId: string, shardBlockNumber: number}>}
*/
async getUnprocessedShardBlock() {
// SELECT shardId, shardBlockNumber from sharchainBlocks WHERE processed = FALSE LIMIT 1
for (let key in this.shardchainBlocks) {
if (this.shardchainBlocks[key] === false) {
const arr = key.split('_');
return {
shardId: arr[0],
shardBlockNumber: Number(arr[1]),
}
}
}
return undefined;
}
/**
* @param address {string}
* @return {Promise<boolean>}
*/
async isPublisher(address) {
return address === 'EQCmDZwk4sS3Nl2VumtvsDMs5AZ2AqH8yqJzf9y10sxaolWg';
}
subscriptions = [];
/**
* @param address {string}
* @return {Promise<boolean>}
*/
async isSubscription(address) {
address = new Address(address).toString(false)
return Boolean(this.subscriptions.find(s => s.address === address));
}
/**
* @param address {string}
* @param data subscriptionData from subscription contract get-method (contains startAt, period, timeout, lastPayment, etc. fields)
*/
async replaceSubscription(address, data) {
data.address = new Address(address).toString(false);
this.subscriptions.push(data);
}
async getSubscriptionsReadyForPayment() {
return this.subscriptions.filter(s => {
const now = Date.now() / 1000; // in seconds
const isPaid = now - s.lastPayment < s.period;
return !isPaid && (now - s.lastRequest > s.timeout);
});
}
}
async function init() {
const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC'));
const storage = new BlocksStorageImpl();
/**
* @param address {string} publisher address
* @param tx tx from `getTransactions`
* @return {Promise<boolean>}
*/
const processTransaction = async (address, tx) => {
const msg = tx.in_msg;
if (!msg) return false;
if (msg.message !== 'ODY6s4A=\n') return false; // msg from subscription prefix
const subscriptionAddress = msg.source; // subscription contract address
const amount = new BN(msg.value); // coins received by the publisher (in nanotons); subscription amount minus fee and withheld 0.0671 ton
// get subscription data
const subscription = new TonWeb.SubscriptionContract(tonweb.provider, {address: subscriptionAddress});
const subscriptionData = await subscription.getSubscriptionData();
// check subscription contract validity
const subscription2 = new TonWeb.SubscriptionContract(tonweb.provider, {
wc: 0,
wallet: new Address(subscriptionData.wallet),
beneficiary: new Address(subscriptionData.beneficiary),
amount: subscriptionData.amount,
period: subscriptionData.period,
startAt: subscriptionData.startAt,
timeout: subscriptionData.timeout,
subscriptionId: subscriptionData.subscriptionId
});
const validSubscriptionAddress = await subscription2.getAddress();
if (new Address(subscriptionAddress).toString(false) !== validSubscriptionAddress.toString(false)) return false;
// todo: check subscription data === bot subscription data for this publisher `address` + `subscriptionId`
// notify
const isNew = !(await storage.isSubscription(subscriptionAddress));
if (isNew) {
await storage.replaceSubscription(subscriptionAddress, subscriptionData);
}
console.log(`Notify: publisher ${address} receive ${TonWeb.utils.fromNano(amount)} from ${isNew ? 'new' : ''} subscriber ${subscriptionData.wallet} via ${subscriptionAddress} subscription contract; subscription ID = ${subscriptionData.subscriptionId}`);
return true;
}
// force process last N transaction of publisher address and find subscriptions
const pollAddress = async (address) => {
const LIMIT = 20;
const txs = await tonweb.provider.getTransactions(address, LIMIT);
for (const tx of txs) {
await processTransaction(address, tx);
}
}
await pollAddress('EQCmDZwk4sS3Nl2VumtvsDMs5AZ2AqH8yqJzf9y10sxaolWg');
// subscribe to new network blocks and find subscriptions
const onTransaction = async (shortTx) => {
const address = shortTx.account;
if (await storage.isPublisher(address)) {
const txs = await tonweb.provider.getTransactions(address, 1, shortTx.lt, shortTx.hash);
const tx = txs[0];
if (tx) {
await processTransaction(address, tx);
} else {
console.error('Cant get tx', shortTx);
}
}
}
const blockSubscribe = new TonWeb.BlockSubscribe(tonweb.provider, storage, onTransaction);
await blockSubscribe.start();
// payments interval
let isPaymentsProcessing = false;
const paymentsTick = async () => {
if (isPaymentsProcessing) return;
isPaymentsProcessing = true;
try {
const subscriptions = await storage.getSubscriptionsReadyForPayment();
for (const s of subscriptions) {
const subscription = new TonWeb.SubscriptionContract(tonweb.provider, {address: s.address});
const subscriptionData = await subscription.getSubscriptionData();
storage.replaceSubscription(s.address, subscriptionData);
await subscription.methods.pay().send();
}
} catch (e) {
console.error(e);
}
isPaymentsProcessing = false;
}
setInterval(() => paymentsTick(), 60 * 1000); // 1 min
}
init();