Skip to content

Commit

Permalink
Start of modified Contract class
Browse files Browse the repository at this point in the history
The `Contract` now requires an ABI, Account, and Client

The `call` method is currently not implemented.
  • Loading branch information
aaroncox committed Jul 7, 2023
1 parent a84aa69 commit 62b0841
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 214 deletions.
171 changes: 55 additions & 116 deletions src/contract.ts
Original file line number Diff line number Diff line change
@@ -1,148 +1,87 @@
import {
ABI,
ABISerializableObject,
Action,
Name,
NameType,
Session,
TransactResult,
} from '@wharfkit/session'
import type {APIClient} from '@wharfkit/session'
import {ABI, ABIDef, APIClient, Name, NameType, Session} from '@wharfkit/session'

import {Table} from './contract/table'

export interface ContractArgs {
abi: ABIDef
account: NameType
client: APIClient
}

export interface ContractOptions {
name: NameType
client?: APIClient
abi?: ABI.Def
session?: Session
}

/**
* Represents a smart contract in the blockchain.
* Represents a smart contract deployed to a specific blockchain.
* Provides methods for interacting with the contract such as
* calling actions, reading tables, and getting the ABI of the contract.
*/
export class Contract {
private static _shared: Contract | null = null
private static account: Name

private abi?: ABI.Def

readonly abi: ABI
readonly account: Name
readonly client?: APIClient
readonly client: APIClient

/**
* Constructs a new `Contract` instance.
*
* @param {ContractArgs} args - The required arguments for a contract.
* @param {ContractOptions} options - The options for the contract.
* @param {NameType} options.name - The name of the contract.
* @param {APIClient} options.client - The client to connect to the blockchain.
*/
constructor(options: ContractOptions) {
this.account = Name.from(options.name)

this.client = options.client

this.abi = options.abi
}

/**
* Creates a new `Contract` instance with the given options.
*
* @param {ContractOptions} options - The options for the contract.
* @return {Contract} A new contract instance.
*/
static from(options: ContractOptions): Contract {
return new this(options)
}

/**
* Calls a contract action.
*
* @param {NameType} name - The name of the action.
* @param {ABISerializableObject | {[key: string]: any}} data - The data for the action.
* @param {Session} session - The session object to use to sign the transaction.
* @return {Promise<TransactResult>} A promise that resolves with the transaction data.
*/
async call(
name: NameType,
data: ABISerializableObject | {[key: string]: any},
session: Session
): Promise<TransactResult> {
const action: Action = Action.from({
account: this.account,
name,
authorization: [],
data,
})

// Trigger the transaction using the session kit
return session.transact({action})
}

/**
* Gets all the tables for the contract.
*
* @return {Promise<Table[]>} A promise that resolves with all the tables for the contract.
*/
async getTables(): Promise<Table[]> {
const abi = await this.getAbi()
constructor(args: ContractArgs, options: ContractOptions = {}) {
this.abi = ABI.from(args.abi)
this.account = Name.from(args.account)
this.client = args.client

return abi.tables.map((table) => {
return new Table({
this.abi.tables.forEach((tableDef) => {
this.tables[String(tableDef.name)] = Table.from({
contract: this,
name: table.name,
name: tableDef.name,
rowType: tableDef.type,
})
})
}

/**
* Gets a specific table for the contract.
*
* @param {NameType} name - The name of the table.
* @return {Promise<Table>} A promise that resolves with the specified table.
*/
async getTable(name: NameType): Promise<Table> {
const tables = await this.getTables()
static from(args: ContractArgs, options: ContractOptions = {}): Contract {
return new this(args, options)
}

const table = tables.find((table) => table.name.equals(name))
get tables(): string[] {
return this.abi.tables.map((table) => String(table.name))
}

if (!table) {
throw new Error(`No table found with name ${name}`)
public table(name: NameType) {
if (!this.tables.includes(String(name))) {
throw new Error(`Contract (${this.account}) does not have a table named (${name})`)
}

return table
return Table.from({
contract: this,
name,
})
}

// TODO: reimplement call method
/**
* Gets the ABI for the contract.
* Calls a contract action.
*
* @return {Promise<ABI.Def>} A promise that resolves with the ABI for the contract.
* @param {NameType} name - The name of the action.
* @param {ABISerializableObject | {[key: string]: any}} data - The data for the action.
* @param {Session} session - The session object to use to sign the transaction.
* @return {Promise<TransactResult>} A promise that resolves with the transaction data.
*/
async getAbi(): Promise<ABI.Def> {
if (this.abi) {
return this.abi
}

if (!this.client) {
throw new Error('Cannot get ABI without client')
}

let response

try {
response = await this.client.v1.chain.get_abi(this.account)
} catch (error: any) {
if (error.message.includes('Account not found')) {
throw new Error(`No ABI found for ${this.account}`)
} else {
throw new Error(`Error fetching ABI: ${JSON.stringify(error)}`)
}
}

const {abi} = response

this.abi = abi

return abi
}
// async call(
// name: NameType,
// data: ABISerializableObject | {[key: string]: any},
// session: Session
// ): Promise<TransactResult> {
// const action: Action = Action.from({
// account: this.account,
// name,
// authorization: [],
// data,
// })

// // Trigger the transaction using the session kit
// return session.transact({action})
// }
}
2 changes: 1 addition & 1 deletion src/contract/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export class Table<TableRow extends ABISerializableConstructor = ABISerializable
'Contract must be passed as a parameter in order for getAbi to be called.'
)
}
return this.contract.getAbi()
return this.contract.abi
}

private async getAbiTable(): Promise<ABI.Table | undefined> {
Expand Down
6 changes: 3 additions & 3 deletions src/kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ export class ContractKit {
* @returns
*/
async load(contract: NameType): Promise<Contract> {
const name = Name.from(contract)
const abiDef = await this.abiCache.getAbi(name)
const account = Name.from(contract)
const abiDef = await this.abiCache.getAbi(account)
return new Contract({
abi: ABI.from(abiDef),
account,
client: this.client,
name,
})
}
}
130 changes: 49 additions & 81 deletions test/tests/contract.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,77 @@
import {assert} from 'chai'
import {makeClient, makeMockAction, mockSession} from '@wharfkit/mock-data'

import {Contract, Table} from '$lib'
import ContractKit, {Contract, ContractArgs, Table} from '$lib'
import {ABI, Name, Serializer} from '@wharfkit/session'

const mockClient = makeClient('https://eos.greymass.com')

const mockContractArgs: ContractArgs = {
abi: {version: 'eosio::abi/1.2'},
account: 'eosio',
client: mockClient,
}

suite('Contract', () => {
let mockContract: Contract

setup(async function () {
mockContract = new Contract({
name: 'decentiumorg',
const kit = new ContractKit({
client: mockClient,
})
mockContract = await kit.load('eosio')
})

suite('from', () => {
test('returns a Contract instance', () => {
const contract = Contract.from({
name: 'decentiumorg',
client: mockClient,
})
assert.instanceOf(contract, Contract)
})
})

suite('call', () => {
test('calls a contract action', async () => {
suite('construct', function () {
test('typed', function () {
const contract = new Contract({
name: 'eosio.token',
client: mockClient,
...mockContractArgs,
abi: ABI.from(mockContractArgs.abi),
account: Name.from(mockContractArgs.account),
})
const session = mockSession
const actionName = 'transfer'
const {data} = makeMockAction()
await contract.call(actionName, data, session)
assert.instanceOf(contract, Contract)
})
})

suite('getTables', () => {
test('returns list of tables', async () => {
const tables = await mockContract.getTables()
assert.lengthOf(tables, 5)
assert.instanceOf(tables[0], Table)
test('untyped', function () {
const contract = new Contract(mockContractArgs)
assert.instanceOf(contract, Contract)
})
})

suite('getTable', () => {
test('returns single table', async () => {
assert.instanceOf(await mockContract.getTable('blogs'), Table)
assert.instanceOf(await mockContract.getTable('links'), Table)
assert.instanceOf(await mockContract.getTable('posts'), Table)
suite('tables', function () {
test('list table names', function () {
assert.isArray(mockContract.tables)
assert.lengthOf(mockContract.tables, 26)
assert.isTrue(mockContract.tables.includes('voters'))
})
})

suite('getAbi', () => {
test('returns ABI for the contract', async () => {
const abi = await mockContract.getAbi()

assert.isObject(abi)
assert.hasAllKeys(abi, [
'version',
'types',
'structs',
'actions',
'tables',
'ricardian_clauses',
'error_messages',
'abi_extensions',
'action_results',
'variants',
])

const abiSecondCall = await mockContract.getAbi()
assert.strictEqual(
abi,
abiSecondCall,
'ABI should be cached and return the same instance on subsequent calls'
)
suite('table', function () {
test('load table using Name', function () {
const table = mockContract.table('voters')
assert.instanceOf(table, Table)
assert.isTrue(table.name.equals('voters'))
})

test('throws error when client is not set', async () => {
const contractWithoutClient = new Contract({name: 'decentiumorg'})
try {
await contractWithoutClient.getAbi()
assert.fail('Expected method to reject.')
} catch (err: any) {
assert.strictEqual(err.message, 'Cannot get ABI without client')
}
test('load table using string', function () {
const table = mockContract.table(Name.from('voters'))
assert.instanceOf(table, Table)
assert.isTrue(table.name.equals('voters'))
})

test('throws error when ABI not found', async () => {
const contractWithNonExistentName = new Contract({
name: 'nonExistent',
client: mockClient,
})

try {
await contractWithNonExistentName.getAbi()
} catch (error: any) {
assert.strictEqual(
error.message,
`No ABI found for ${contractWithNonExistentName.account}`
)
}
test('throws on invalid name', function () {
assert.throws(() => mockContract.table('foo'))
})
})

// TODO: reimplement call tests
// suite('call', () => {
// test('calls a contract action', async () => {
// const contract = new Contract({
// name: 'eosio.token',
// client: mockClient,
// })
// const session = mockSession
// const actionName = 'transfer'
// const {data} = makeMockAction()
// await contract.call(actionName, data, session)
// })
// })
})
Loading

0 comments on commit 62b0841

Please sign in to comment.