@0xtemple/client
Version:
Tookit for interacting with vara eps framework
469 lines (435 loc) • 16.5 kB
text/typescript
import {
GearApi,
MessageSendOptions,
PayloadType,
HexString,
GearKeyring,
decodeAddress,
} from '@gear-js/api';
import { blake2AsHex } from '@polkadot/util-crypto';
// import { SubmittableExtrinsic } from '@polkadot/api/types';
import type { SubmittableExtrinsic } from '@polkadot/api/submittable/types';
import { ISubmittableResult } from '@polkadot/types/types';
import { FaucetNetworkType, Network, NodeUrlType } from '../../types';
// import { SuiOwnedObject, SuiSharedObject } from '../suiModel';
import { delay } from './util';
import { Keyring } from '@polkadot/keyring';
import { ProgramMetadata, GearProgram } from '@gear-js/api';
import { ApiPromise, WsProvider, HttpProvider } from '@polkadot/api';
import { metadata } from '@polkadot/types/interfaces/essentials';
import { KeyringPair } from '@polkadot/keyring/types';
function hexToBinary(hex: string): string {
let binaryString = '';
for (let i = 0; i < hex.length; i++) {
const bin = parseInt(hex[i], 16).toString(2).padStart(4, '0');
binaryString += bin;
}
return binaryString;
}
/**
* `SuiTransactionSender` is used to send transaction with a given gas coin.
* It always uses the gas coin to pay for the gas,
* and update the gas coin after the transaction.
*/
export class VaraInteractor {
public readonly clients: GearApi[];
public currentClient: GearApi;
public readonly fullNodes: NodeUrlType[];
public currentFullNode: NodeUrlType;
public readonly wsProviders?: WsProvider[];
public wsProvider?: WsProvider;
public wsApi?: Promise<ApiPromise>;
public network?: Network;
constructor(
fullNodeUrls: NodeUrlType[],
network?: Network,
connectWs?: boolean
) {
if (fullNodeUrls.length === 0)
throw new Error('fullNodeUrls must not be empty');
this.fullNodes = fullNodeUrls;
if (connectWs === true) {
// this.wsProviders = fullNodeUrls.map(
// (providerAddress) => new WsProvider(providerAddress.ws)
// );
// this.wsProvider = this.wsProviders[0];
// this.wsApi = ApiPromise.create({ provider: this.wsProvider });
this.clients = fullNodeUrls.map((fullNodeUrl) => {
return new GearApi({
providerAddress: fullNodeUrl.ws,
noInitWarn: true,
});
});
} else {
this.clients = fullNodeUrls.map((fullNodeUrl) => {
const httpProvider = new HttpProvider(fullNodeUrl.http);
return new GearApi({ provider: httpProvider, noInitWarn: true });
});
}
this.currentFullNode = fullNodeUrls[0];
this.currentClient = this.clients[0];
this.network = network;
}
async nodeInfo() {
for (const clientIdx in this.clients) {
try {
const chain = await this.clients[clientIdx].chain();
const nodeName = await this.clients[clientIdx].nodeName();
const nodeVersion = await this.clients[clientIdx].nodeVersion();
// const genesis = this.clients[clientIdx].genesisHash.toHex();
return {
chain,
nodeName,
nodeVersion,
};
} catch (err) {
console.warn(
`Failed to query node info with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error('Failed to send transaction with all fullnodes');
}
switchToNextClient() {
const currentClientIdx = this.clients.indexOf(this.currentClient);
this.currentClient =
this.clients[(currentClientIdx + 1) % this.clients.length];
this.currentFullNode =
this.fullNodes[(currentClientIdx + 1) % this.clients.length];
}
async signAndSend(
signer: KeyringPair,
// programId: HexString,
tx: SubmittableExtrinsic
) {
for (const clientIdx in this.clients) {
try {
return await tx.signAndSend(signer, (event: any) => {
console.log(event.toHuman());
});
} catch (err) {
console.warn(
`Failed to send transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error('Failed to send transaction with all fullnodes');
}
async structuredTransaction(
signer: KeyringPair,
programId: string,
payload: PayloadType,
metadata: string,
gasLimit?: number,
value?: number
) {
for (const clientIdx in this.clients) {
try {
await delay(1500);
const meta = ProgramMetadata.from(
metadata
// '00020000010000000001070000000100000000000000000109000000010a000000d5072c000c34656e67696e655f736368656d611c73746f726167652c536368656d614576656e7400010c245365745265636f72640c0004011c4163746f724964000010011c5665633c75383e000010011c5665633c75383e0000003044656c6574655265636f7264080004011c4163746f724964000010011c5665633c75383e000100205265676973746572040014015c5665633c284163746f7249642c205665633c75383e293e000200000410106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004000801205b75383b2033325d000008000003200000000c000c0000050300100000020c0014000002180018000004080410001c0838656e67696e655f73797374656d733053797374656d416374696f6e0001080c41646400000038536574456e746974794c6576656c08002001107531323800002001107531323800010000200000050700240838656e67696e655f73797374656d731c5374617465496e000108404765744c6576656c4279456e746974790400200110753132380000004447657443757272656e74436f756e74657200010000280838656e67696e655f73797374656d732053746174654f75740001083843757272656e74436f756e746572040020011075313238000000344c6576656c4279456e7469747904002001107531323800010000'
);
if (gasLimit === undefined) {
const gas = await this.clients[clientIdx].program.calculateGas.handle(
decodeAddress(signer.address),
programId as HexString,
payload,
value,
true,
meta
);
gasLimit = gas.min_limit;
}
if (value === undefined) {
value = 10000000000000;
}
const tx = await this.clients[clientIdx].message.send(
{
destination: programId as HexString,
payload,
gasLimit: gasLimit,
value: value,
},
meta
);
return tx;
} catch (err) {
console.warn(
`Failed to structured transaction with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error('Failed to structured transaction with all fullnodes');
}
async getMetaHash(programId: HexString) {
for (const clientIdx in this.clients) {
try {
await delay(1500);
const metaHash = await this.clients[clientIdx].program.metaHash(
programId
);
return metaHash;
} catch (err) {
console.warn(
`Failed to get metaHash with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error('Failed to get metaHash with all fullnodes');
}
async queryState(programId: string, payload: PayloadType, metadata: string) {
for (const clientIdx in this.clients) {
try {
await delay(1500);
const meta = ProgramMetadata.from(
metadata
// '00020000010000000001070000000100000000000000000109000000010a000000d5072c000c34656e67696e655f736368656d611c73746f726167652c536368656d614576656e7400010c245365745265636f72640c0004011c4163746f724964000010011c5665633c75383e000010011c5665633c75383e0000003044656c6574655265636f7264080004011c4163746f724964000010011c5665633c75383e000100205265676973746572040014015c5665633c284163746f7249642c205665633c75383e293e000200000410106773746418636f6d6d6f6e287072696d6974697665731c4163746f724964000004000801205b75383b2033325d000008000003200000000c000c0000050300100000020c0014000002180018000004080410001c0838656e67696e655f73797374656d733053797374656d416374696f6e0001080c41646400000038536574456e746974794c6576656c08002001107531323800002001107531323800010000200000050700240838656e67696e655f73797374656d731c5374617465496e000108404765744c6576656c4279456e746974790400200110753132380000004447657443757272656e74436f756e74657200010000280838656e67696e655f73797374656d732053746174654f75740001083843757272656e74436f756e746572040020011075313238000000344c6576656c4279456e7469747904002001107531323800010000'
);
// console.log(meta.getAllTypes());
const state = await this.clients[clientIdx].programState.read(
{
programId: programId as HexString,
payload,
// payload: {
// GetCurrentCounter: null,
// },
},
meta
);
return state.toHuman();
} catch (err) {
console.warn(
`Failed to get metaHash with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error('Failed to get metaHash with all fullnodes');
}
async queryBalance(addr: String) {
for (const clientIdx in this.clients) {
try {
let balances_amount = await this.clients[
clientIdx
].query.system.account(addr);
return balances_amount.toHuman();
} catch (err) {
console.warn(
`Failed to query(${addr}) balance with fullnode ${this.fullNodes[clientIdx]}: ${err}`
);
await delay(2000);
}
}
throw new Error(`Failed to query(${addr}) balance with all fullnodes`);
}
// async getObjects(
// ids: string[],
// options?: SuiObjectDataOptions
// ): Promise<SuiObjectData[]> {
// const opts: SuiObjectDataOptions = options ?? {
// showContent: true,
// showDisplay: true,
// showType: true,
// showOwner: true,
// };
// for (const clientIdx in this.clients) {
// try {
// const objects = await this.clients[clientIdx].multiGetObjects({
// ids,
// options: opts,
// });
// const parsedObjects = objects
// .map((object) => {
// return object.data;
// })
// .filter((object) => object !== null && object !== undefined);
// return parsedObjects as SuiObjectData[];
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get objects with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get objects with all fullnodes');
// }
// async getObject(id: string) {
// const objects = await this.getObjects([id]);
// return objects[0];
// }
// async getDynamicFieldObject(
// parentId: string,
// name: RpcTypes.DynamicFieldName
// ) {
// for (const clientIdx in this.clients) {
// try {
// return await this.clients[clientIdx].getDynamicFieldObject({
// parentId,
// name,
// });
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get DynamicFieldObject with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get DynamicFieldObject with all fullnodes');
// }
// async getDynamicFields(parentId: string, cursor?: string, limit?: number) {
// for (const clientIdx in this.clients) {
// try {
// return await this.clients[clientIdx].getDynamicFields({
// parentId,
// cursor,
// limit,
// });
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get DynamicFields with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get DynamicFields with all fullnodes');
// }
// async getTxDetails(digest: string) {
// for (const clientIdx in this.clients) {
// try {
// const txResOptions: SuiTransactionBlockResponseOptions = {
// showEvents: true,
// showEffects: true,
// showObjectChanges: true,
// showBalanceChanges: true,
// };
// return await this.clients[clientIdx].getTransactionBlock({
// digest,
// options: txResOptions,
// });
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get TransactionBlocks with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get TransactionBlocks with all fullnodes');
// }
// async getOwnedObjects(owner: string, cursor?: string, limit?: number) {
// for (const clientIdx in this.clients) {
// try {
// return await this.clients[clientIdx].getOwnedObjects({
// owner,
// cursor,
// limit,
// });
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get OwnedObjects with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get OwnedObjects with all fullnodes');
// }
// async getNormalizedMoveModulesByPackage(packageId: string) {
// for (const clientIdx in this.clients) {
// try {
// return await this.clients[clientIdx].getNormalizedMoveModulesByPackage({
// package: packageId,
// });
// } catch (err) {
// await delay(2000);
// console.warn(
// `Failed to get NormalizedMoveModules with fullnode ${this.fullNodes[clientIdx]}: ${err}`
// );
// }
// }
// throw new Error('Failed to get NormalizedMoveModules with all fullnodes');
// }
// /**
// * @description Update objects in a batch
// * @param suiObjects
// */
// async updateObjects(suiObjects: (SuiOwnedObject | SuiSharedObject)[]) {
// const objectIds = suiObjects.map((obj) => obj.objectId);
// const objects = await this.getObjects(objectIds);
// for (const object of objects) {
// const suiObject = suiObjects.find(
// (obj) => obj.objectId === object?.objectId
// );
// if (suiObject instanceof SuiSharedObject) {
// if (
// object.owner &&
// typeof object.owner === 'object' &&
// 'Shared' in object.owner
// ) {
// suiObject.initialSharedVersion =
// object.owner.Shared.initial_shared_version;
// } else {
// suiObject.initialSharedVersion = undefined;
// }
// } else if (suiObject instanceof SuiOwnedObject) {
// suiObject.version = object?.version;
// suiObject.digest = object?.digest;
// }
// }
// }
// /**
// * @description Select coins that add up to the given amount.
// * @param addr the address of the owner
// * @param amount the amount that is needed for the coin
// * @param coinType the coin type, default is '0x2::SUI::SUI'
// */
// async selectCoins(
// addr: string,
// amount: number,
// coinType: string = '0x2::SUI::SUI'
// ) {
// const selectedCoins: {
// objectId: string;
// digest: string;
// version: string;
// }[] = [];
// let totalAmount = 0;
// let hasNext = true,
// nextCursor: string | null | undefined = null;
// while (hasNext && totalAmount < amount) {
// const coins = await this.currentClient.getCoins({
// owner: addr,
// coinType: coinType,
// cursor: nextCursor,
// });
// // Sort the coins by balance in descending order
// coins.data.sort((a, b) => parseInt(b.balance) - parseInt(a.balance));
// for (const coinData of coins.data) {
// selectedCoins.push({
// objectId: coinData.coinObjectId,
// digest: coinData.digest,
// version: coinData.version,
// });
// totalAmount = totalAmount + parseInt(coinData.balance);
// if (totalAmount >= amount) {
// break;
// }
// }
// nextCursor = coins.nextCursor;
// hasNext = coins.hasNextPage;
// }
// if (!selectedCoins.length) {
// throw new Error('No valid coins found for the transaction.');
// }
// return selectedCoins;
// }
// async requestFaucet(address: string, network: FaucetNetworkType) {
// await requestSuiFromFaucetV0({
// host: getFaucetHost(network),
// recipient: address,
// });
// }
}