vasku
Version:
TVM-Solidity contract development framework
446 lines (403 loc) • 11.6 kB
text/typescript
import {
type AbiContract,
type DecodedMessageBody,
type KeyPair, type ResultOfProcessMessage,
type TonClient
} from '@eversdk/core'
import { type Giver } from '../giver'
import { Global } from '../global'
import { error } from './constants'
import { createPayload } from './payload'
import { generateRandomKeyPair } from '../utils/generateRandomKeyPair'
export enum AccountType {
notFound = '-1',
unInit = '0',
active = '1',
frozen = '2',
nonExist = '3'
}
export type CompiledContractConfig = {
address?: string
initial?: Record<string, any>
keys?: KeyPair
}
export type ContractOptions = {
client?: TonClient
giver?: Giver
}
export interface ResultOfCall {
out: any
result: ResultOfProcessMessage
}
export class Contract {
private readonly abi: {
type: 'Contract'
value: AbiContract
}
private readonly initial?: Record<string, any>
private readonly tvc?: string
private readonly client?: TonClient
private readonly giver?: Giver
private _address?: string
private _keys?: KeyPair
private _lastTransactionLogicTime: string
constructor (
config: {
address: string
keys?: KeyPair
abi?: AbiContract
initial?: Record<string, any>
tvc?: string
} | {
address?: string
keys?: KeyPair
abi: AbiContract
initial: Record<string, any>
tvc: string
},
options: {
client?: TonClient
giver?: Giver
} = {}
) {
this._address = config.address
this._keys = config.keys
this._lastTransactionLogicTime = '0'
this.abi = {
type: 'Contract',
value: config.abi ?? {}
}
this.initial = config.initial ?? {}
this.tvc = config.tvc
this.client = options.client ?? Global.client
this.giver = options.giver ?? Global.giver
}
/**
* Calculates the address only once. Next time returns the already calculated address
* You can use this if you want to know the address of the contract before deploying
* @example
* const contract = new Contract(...)
* const address = await contract.address()
* @return
* '0:97b53be2604579e89bd0077a5456456857792eb2ff09849d14321fc2c167f29e'
*/
public async address (): Promise<string> {
if (this._address !== undefined)
return this._address
if (this.client === undefined)
throw error.noClient
if (this.tvc === undefined)
throw new Error('Contract tvc is undefined')
this._address = (await this.client.abi.encode_message({
abi: this.abi,
signer: {
type: 'Keys',
keys: await this.keys()
},
deploy_set: {
tvc: this.tvc,
initial_data: this.initial
}
})).address
return this._address
}
/**
* Generates a random key pair if no keys are given. Next time returns the already generated key pair
* @example
* const contract = new Contract(...)
* const address = await contract.keyPair()
* @return
* '0:97b53be2604579e89bd0077a5456456857792eb2ff09849d14321fc2c167f29e'
*/
public async keys (): Promise<KeyPair> {
if (this._keys !== undefined)
return this._keys
if (this.client === undefined)
throw error.noClient
if (this.tvc === undefined)
throw new Error('Contract tvc is undefined')
this._keys = await generateRandomKeyPair(this.client)
return this._keys
}
/**
* Return contract balance
* @example
* const contract = new Contract(...)
* const address = await contract.balance()
* @return
* 10000000000n
*/
public async balance (): Promise<bigint> {
if (this.client === undefined)
throw error.noClient
const result: any[] = (await this.client.net.query_collection({
collection: 'accounts',
filter: {
id: {
eq: await this.address()
},
last_trans_lt: {
ge: this._lastTransactionLogicTime
}
},
result: 'balance'
})).result
return (result !== undefined && result.length > 0) ? BigInt(result[0].balance) : BigInt(0)
}
/**
* Return contract account type
* @example
* const contract = new Contract(...)
* const address = await contract.accountType()
* @return
* 1
*/
public async accountType (): Promise<AccountType> {
if (this.client === undefined)
throw error.noClient
const result: any[] = (await this.client.net.query_collection({
collection: 'accounts',
filter: {
id: {
eq: await this.address()
},
last_trans_lt: {
ge: this._lastTransactionLogicTime
}
},
result: 'acc_type'
})).result
return (result !== undefined && result.length > 0) ? result[0].acc_type.toString() : AccountType.nonExist
}
/**
* Run contract locally
* @param method Method name
* @param input
* @example
* const contract = new Contract(...)
* const address = await contract.runMethod('getHistory', { offset: 1 })
* @return { DecodedMessageBody }
*/
public async runMethod (method: string, input: any = {}): Promise<DecodedMessageBody> {
if (this.client === undefined)
throw error.noClient
//////////////////////
// Read Bag Of Cell //
//////////////////////
const bagOfCell = (await this.client.net.query_collection({
collection: 'accounts',
filter: {
id: {
eq: await this.address()
}
},
result: 'boc'
})).result[0].boc
/////////////////////////////
// Encode external message //
/////////////////////////////
const encodedMessage = await this.client.abi.encode_message({
abi: this.abi,
signer: {
type: 'None'
},
call_set: {
function_name: method,
input
},
address: this._address
})
/////////////////////////////////////////
// Execute contract on virtual machine //
/////////////////////////////////////////
const outputMessage = (await this.client.tvm.run_tvm({
message: encodedMessage.message,
account: bagOfCell
})).out_messages[0]
///////////////////////////
// Decode output message //
///////////////////////////
return await this.client.abi.decode_message({
abi: this.abi,
message: outputMessage
})
}
/**
* External call
* @param method Method name
* @param input
* @param [keys] Use it if you want to call contact with keys. `this.keys` used by default.
* @example
* const contract = new Contract(...)
* const address = await contract.runMethod('getHistory', { offset: 1 })
*/
public async callMethod (method: string, input: any = {}, keys?: KeyPair): Promise<{
out: any
result: ResultOfProcessMessage
}> {
if (this.client === undefined)
throw error.noClient
///////////////////////////////
// Generate external message //
///////////////////////////////
const resultOfProcessMessage: ResultOfProcessMessage = await this.client.processing.process_message({
message_encode_params: {
abi: this.abi,
signer: {
type: 'Keys',
keys: keys ?? await this.keys()
},
address: await this.address(),
call_set: {
function_name: method,
input
}
},
send_events: false
})
////////////////////
// Decode message //
////////////////////
this._lastTransactionLogicTime = resultOfProcessMessage.transaction.lt.toString() ?? this._lastTransactionLogicTime
return {
out: resultOfProcessMessage.decoded?.output ?? {},
result: resultOfProcessMessage
}
}
/**
* Return last transaction logic time
* @example
* const contract = new Contract(...)
* contract.lastTransactionLogicTime
* @return
* '0'
*/
get lastTransactionLogicTime (): string {
return this._lastTransactionLogicTime
}
/**
* Deploy
* @param value Deployment in nano coins
* @param input constructor data
* @param useGiver Send coins from giver before deployment if true
* @param timeout How long to wait coins from giver on contract in milliseconds
*/
public async _deploy (
value: string | number | bigint,
input: any = {},
useGiver: boolean = true,
timeout: number = 60000
): Promise<ResultOfProcessMessage> {
////////////////////////////
// Check all requirements //
////////////////////////////
const accountType = await this.accountType()
if (accountType === AccountType.active)
throw error.contractAlreadyDeployed
if (this.client === undefined)
throw error.noClient
if (this.tvc === undefined)
throw error.noTvc
if (useGiver && this.giver === undefined)
throw error.noGiver
///////////////
// Use giver //
///////////////
const to = await this.address()
if (useGiver && this.giver !== undefined)
await this.giver.send({ to, value })
/////////////////////////////
// Waiting for balance > 0 //
/////////////////////////////
await this.client.net.wait_for_collection({
collection: 'accounts',
filter: {
id: {
eq: await this.address()
},
data: {
eq: null
},
code: {
eq: null
},
balance: {
gt: '0'
}
},
result: 'last_trans_lt',
timeout
})
////////////
// Deploy //
////////////
const resultOfProcessMessage: ResultOfProcessMessage = await this.client.processing.process_message({
message_encode_params: {
abi: this.abi,
signer: {
type: 'Keys',
keys: await this.keys()
},
deploy_set: {
tvc: this.tvc,
initial_data: this.initial
},
call_set: {
function_name: 'constructor',
input
}
},
send_events: false
})
this._lastTransactionLogicTime = resultOfProcessMessage.transaction.lt ?? this._lastTransactionLogicTime
return resultOfProcessMessage
}
/**
* Use this if you want to wait for a transaction from one contract to another
* @example
* const sender = new SenderContract(...)
* const receiver = new ReceiverContract(...)
* const to = await receiver.address()
* await sender.call.send({to, value: 1_000_000_000})
* const waitingResult = await receiver.wait(5000)
* @param timeout Time in milliseconds
* @return
* true
*/
public async wait (timeout: number = 60000): Promise<boolean> {
if (this.client === undefined)
throw error.noClient
try {
const queryCollectionResult = await this.client.net.wait_for_collection({
collection: 'accounts',
filter: {
id: {
eq: await this.address()
},
last_trans_lt: {
gt: this._lastTransactionLogicTime
}
},
result: 'last_trans_lt',
timeout
})
const result = queryCollectionResult.result
this._lastTransactionLogicTime = result.last_trans_lt ?? '0'
return true
} catch (e: any) {}
return false
}
/**
* Create payload
* @example
* const contract = new Contract(...)
* const payload = await contract.createPayload('bet', { luckyNumber: 5})
* @param method Method name
* @param input
*/
public async createPayload (method: string, input: any = {}): Promise<string> {
if (this.client === undefined)
throw error.noClient
return await createPayload(this.abi.value, method, input, this.client)
}
}