// Copyright (C) 2018 Zilliqa // // This file is part of zilliqa-js // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . import { Transaction, TxStatus, Wallet } from '@zilliqa-js/account'; import { GET_TX_ATTEMPTS, Provider, RPCMethod, sign } from '@zilliqa-js/core'; import { fromBech32Address, isValidChecksumAddress, normaliseAddress, toChecksumAddress, } from '@zilliqa-js/crypto'; import { BN, validation } from '@zilliqa-js/util'; import { Blockchain } from '@zilliqa-js/blockchain'; import { Contracts } from './factory'; import { ABI, CallParams, ContractStatus, DeployError, DeployParams, DeploySuccess, Init, State, Value, } from './types'; const NIL_ADDRESS = '0x0000000000000000000000000000000000000000'; export class Contract { factory: Contracts; provider: Provider; signer: Wallet; blockchain: Blockchain; init: Init; abi?: ABI; state?: State; address?: string; code?: string; status: ContractStatus; error?: any; constructor( factory: Contracts, code?: string, abi?: ABI, address?: string, init?: any, state?: any, checkAddr: boolean = false, ) { this.factory = factory; this.provider = factory.provider; this.signer = factory.signer; this.blockchain = new Blockchain(factory.provider, factory.signer); // assume that we are accessing an existing contract if (address) { this.abi = abi; if (checkAddr) { this.address = normaliseAddress(address); } else { if (validation.isBech32(address)) { this.address = fromBech32Address(address); } else if (isValidChecksumAddress(address)) { this.address = address; } else { this.address = toChecksumAddress(address); } } this.init = init; this.state = state; this.status = ContractStatus.Deployed; } else { // assume we're deploying this.abi = abi; this.code = code; this.init = init; this.status = ContractStatus.Initialised; } } /** * isInitialised * * Returns true if the contract has not been deployed * * @returns {boolean} */ isInitialised(): boolean { return this.status === ContractStatus.Initialised; } /** * isDeployed * * Returns true if the contract is deployed * * @returns {boolean} */ isDeployed(): boolean { return this.status === ContractStatus.Deployed; } /** * isRejected * * Returns true if an attempt to deploy the contract was made, but the * underlying transaction was unsuccessful. * * @returns {boolean} */ isRejected(): boolean { return this.status === ContractStatus.Rejected; } @sign async prepareTx( tx: Transaction, attempts: number = GET_TX_ATTEMPTS, interval: number = 1000, isDeploy: boolean, ): Promise { const response = await this.provider.send( RPCMethod.CreateTransaction, { ...tx.txParams, priority: tx.toDS }, ); if (response.error) { this.address = undefined; this.error = response.error; return tx.setStatus(TxStatus.Rejected); } if (isDeploy) { this.address = response.result.ContractAddress ? toChecksumAddress(response.result.ContractAddress) : undefined; } return tx.confirm(response.result.TranID, attempts, interval); } @sign async prepare(tx: Transaction): Promise { const response = await this.provider.send( RPCMethod.CreateTransaction, { ...tx.txParams, priority: tx.toDS }, ); if (response.error || !response.result) { this.address = undefined; this.error = response.error; tx.setStatus(TxStatus.Rejected); } else { tx.id = response.result.TranID; tx.setStatus(TxStatus.Pending); return response.result.ContractAddress; } } /** * deploy smart contract with no confirm * @param params * @param toDs */ async deployWithoutConfirm( params: DeployParams, toDs: boolean = false, ): Promise<[Transaction, Contract]> { if (!this.code || !this.init) { throw new Error( 'Cannot deploy without code or initialisation parameters.', ); } const tx = new Transaction( { ...params, toAddr: NIL_ADDRESS, amount: new BN(0), code: this.code, data: JSON.stringify(this.init).replace(/\\"/g, '"'), }, this.provider, TxStatus.Initialised, toDs, ); try { this.address = await this.prepare(tx); this.status = this.address === undefined ? ContractStatus.Rejected : ContractStatus.Initialised; return [tx, this]; } catch (err) { throw err; } } /** * deploy * * @param {DeployParams} params * @returns {Promise} */ async deploy( params: DeployParams, attempts: number = 33, interval: number = 1000, toDs: boolean = false, ): Promise<[Transaction, Contract]> { if (!this.code || !this.init) { throw new Error( 'Cannot deploy without code or initialisation parameters.', ); } try { const tx = await this.prepareTx( new Transaction( { ...params, toAddr: NIL_ADDRESS, amount: new BN(0), code: this.code, data: JSON.stringify(this.init).replace(/\\"/g, '"'), }, this.provider, TxStatus.Initialised, toDs, ), attempts, interval, true, ); if (tx.isRejected()) { this.status = ContractStatus.Rejected; this.address = undefined; return [tx, this]; } this.status = ContractStatus.Deployed; this.address = this.address && isValidChecksumAddress(this.address) ? this.address : Contracts.getAddressForContract(tx); return [tx, this]; } catch (err) { throw err; } } async callWithoutConfirm( transition: string, args: Value[], params: CallParams, toDs: boolean = false, ): Promise { const data = { _tag: transition, params: args, }; if (this.error) { return Promise.reject(this.error); } if (!this.address) { return Promise.reject('Contract has not been deployed!'); } const tx = new Transaction( { ...params, toAddr: this.address, data: JSON.stringify(data), }, this.provider, TxStatus.Initialised, toDs, ); try { await this.prepare(tx); return tx; } catch (err) { throw err; } } /** * call * * @param {string} transition * @param {any} params * @returns {Promise} */ async call( transition: string, args: Value[], params: CallParams, attempts: number = 33, interval: number = 1000, toDs: boolean = false, ): Promise { const data = { _tag: transition, params: args, }; if (this.error) { return Promise.reject(this.error); } if (!this.address) { return Promise.reject('Contract has not been deployed!'); } try { return await this.prepareTx( new Transaction( { ...params, toAddr: this.address, data: JSON.stringify(data), }, this.provider, TxStatus.Initialised, toDs, ), attempts, interval, false, ); } catch (err) { throw err; } } async getState(): Promise { if (this.status !== ContractStatus.Deployed) { return Promise.resolve([]); } if (!this.address) { throw new Error('Cannot get state of uninitialised contract'); } const response = await this.blockchain.getSmartContractState(this.address); return response.result; } async getSubState(variableName: string, indices?: string[]): Promise { if (this.status !== ContractStatus.Deployed) { return Promise.resolve([]); } if (!this.address) { throw new Error('Cannot get state of uninitialised contract'); } if (!variableName) { throw new Error('Variable name required'); } const response = await this.blockchain.getSmartContractSubState( this.address, variableName, indices, ); return response.result; } async getInit(): Promise { if (this.status !== ContractStatus.Deployed) { return Promise.resolve([]); } if (!this.address) { throw new Error('Cannot get state of uninitialised contract'); } const response = await this.blockchain.getSmartContractInit(this.address); return response.result; } }