@concordium/node-sdk
Advanced tools
Comparing version
# Changelog | ||
## 1.1.0 2022-06-14 | ||
### Added | ||
- Support for the Invoke contract node entrypoint. | ||
### Fixed | ||
- Lossy parsing of uint64's from the node, if their value was above MAX_SAFE_INTEGER. | ||
## 1.0.0 2022-05-11 | ||
@@ -5,0 +14,0 @@ |
@@ -10,3 +10,2 @@ { | ||
"node_sdk_helpers.js", | ||
"node_sdk_helpers_bg.js", | ||
"node_sdk_helpers.d.ts" | ||
@@ -13,0 +12,0 @@ ], |
@@ -916,2 +916,42 @@ # concordium-node-sdk-js | ||
## InvokeContract | ||
Used to simulate a contract update, and to trigger view functions. | ||
```js | ||
const blockHash = "7f7409679e53875567e2ae812c9fcefe90ced8961d08554756f42bf268a42749"; | ||
const contractAddress = { index: 1n, subindex: 0n }; | ||
const invoker = new AccountAddress('3tXiu8d4CWeuC12irAB7YVb1hzp3YxsmmmNzzkdujCPqQ9EjDm'); | ||
const result = await client.invokeContract( | ||
blockHash, | ||
{ | ||
invoker: invoker, | ||
contract: contractAddress, | ||
method: 'PiggyBank.smash', | ||
amount: undefined, | ||
parameter: undefined, | ||
energy: 30000n, | ||
} | ||
); | ||
if (!result) { | ||
// The node could not attempt the invocation, most likely the contract doesn't exist. | ||
} | ||
if (result.tag === 'failure') { | ||
// Invoke was unsuccesful | ||
const rejectReason = result.reason; // Describes why the update failed; | ||
... | ||
} else { | ||
const events = result.events; // a list of events that would be generated by the update | ||
const returnValue = result.returnValue; // If the invoked method has return value | ||
... | ||
} | ||
``` | ||
Note that some of the parts of the context are optional: | ||
- amount: defaults to 0 | ||
- energy: defaults to 10 million | ||
- parameter: defaults to no parameters | ||
- invoker: uses the zero account address, which can be used instead of finding a random address. | ||
## Deserialize contract state | ||
@@ -918,0 +958,0 @@ The following example demonstrates how to deserialize a contract's state: |
@@ -6,3 +6,3 @@ import { ChannelCredentials, Metadata } from '@grpc/grpc-js'; | ||
import { PeerListResponse } from '../grpc/concordium_p2p_rpc_pb'; | ||
import { AccountInfo, AccountTransaction, AccountTransactionSignature, ArInfo, BlockInfo, BlockSummary, ConsensusStatus, ContractAddress, CredentialDeploymentTransaction, CryptographicParameters, IpInfo, NextAccountNonce, TransactionStatus, Versioned, InstanceInfo, BakerId, PoolStatus, BakerPoolStatus, RewardStatus, PassiveDelegationStatus } from './types'; | ||
import { AccountInfo, AccountTransaction, AccountTransactionSignature, ArInfo, BlockInfo, BlockSummary, ConsensusStatus, ContractAddress, CredentialDeploymentTransaction, CryptographicParameters, IpInfo, NextAccountNonce, TransactionStatus, Versioned, InstanceInfo, BakerId, PoolStatus, BakerPoolStatus, RewardStatus, PassiveDelegationStatus, ContractContext, InvokeContractResult } from './types'; | ||
import { ModuleReference } from './types/moduleReference'; | ||
@@ -171,3 +171,16 @@ /** | ||
getModuleSource(blockHash: string, moduleReference: ModuleReference): Promise<Uint8Array>; | ||
/** | ||
* Invokes a smart contract. | ||
* @param blockHash the block hash at which the contract should be invoked at. The contract is invoked in the state at the end of this block. | ||
* @param context the collection of details used to invoke the contract. Must include the address of the contract and the method invoked. | ||
* @returns If the node was able to invoke, then a object describing the outcome is returned. | ||
* The outcome is determined by the `tag` field, which is either `success` or `failure`. | ||
* The `usedEnergy` field will always be present, and is the amount of NRG was used during the execution. | ||
* If the tag is `success`, then an `events` field is present, and it contains the events that would have been generated. | ||
* If invoking a V1 contract and it produces a return value, it will be present in the `returnValue` field. | ||
* If the tag is `failure`, then a `reason` field is present, and it contains the reason the update would have been rejected. | ||
* If either the block does not exist, or then node fails to parse of any of the inputs, then undefined is returned. | ||
*/ | ||
invokeContract(blockHash: string, contractContext: ContractContext): Promise<InvokeContractResult | undefined>; | ||
sendRequest<T>(command: any, input: T): Promise<Uint8Array>; | ||
} |
@@ -439,2 +439,58 @@ "use strict"; | ||
} | ||
/** | ||
* Invokes a smart contract. | ||
* @param blockHash the block hash at which the contract should be invoked at. The contract is invoked in the state at the end of this block. | ||
* @param context the collection of details used to invoke the contract. Must include the address of the contract and the method invoked. | ||
* @returns If the node was able to invoke, then a object describing the outcome is returned. | ||
* The outcome is determined by the `tag` field, which is either `success` or `failure`. | ||
* The `usedEnergy` field will always be present, and is the amount of NRG was used during the execution. | ||
* If the tag is `success`, then an `events` field is present, and it contains the events that would have been generated. | ||
* If invoking a V1 contract and it produces a return value, it will be present in the `returnValue` field. | ||
* If the tag is `failure`, then a `reason` field is present, and it contains the reason the update would have been rejected. | ||
* If either the block does not exist, or then node fails to parse of any of the inputs, then undefined is returned. | ||
*/ | ||
async invokeContract(blockHash, contractContext) { | ||
if (!(0, util_1.isValidHash)(blockHash)) { | ||
throw new Error('The input was not a valid hash: ' + blockHash); | ||
} | ||
let invoker; | ||
if (!contractContext.invoker) { | ||
invoker = null; | ||
} | ||
else if (contractContext.invoker.address) { | ||
invoker = { | ||
type: 'AddressAccount', | ||
address: contractContext.invoker.address, | ||
}; | ||
} | ||
else { | ||
const invokerContract = contractContext.invoker; | ||
invoker = { | ||
type: 'AddressContract', | ||
address: { | ||
subindex: invokerContract.subindex.toString(), | ||
index: invokerContract.index.toString(), | ||
}, | ||
}; | ||
} | ||
const requestObject = new concordium_p2p_rpc_pb_1.InvokeContractRequest(); | ||
requestObject.setBlockHash(blockHash); | ||
requestObject.setContext((0, util_1.stringToInt)(JSON.stringify({ | ||
invoker, | ||
contract: { | ||
subindex: contractContext.contract.subindex.toString(), | ||
index: contractContext.contract.index.toString(), | ||
}, | ||
amount: contractContext.amount && | ||
contractContext.amount.microGtuAmount.toString(), | ||
method: contractContext.method, | ||
parameter: contractContext.parameter && | ||
contractContext.parameter.toString('hex'), | ||
energy: contractContext.energy && | ||
Number(contractContext.energy.toString()), | ||
}), ['index', 'subindex'])); | ||
const response = await this.sendRequest(this.client.invokeContract, requestObject); | ||
const bigIntPropertyKeys = ['usedEnergy', 'index', 'subindex']; | ||
return (0, util_1.unwrapJsonResponse)(response, (0, util_1.buildJsonResponseReviver)([], bigIntPropertyKeys), (0, util_1.intToStringTransformer)(bigIntPropertyKeys)); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types | ||
@@ -441,0 +497,0 @@ sendRequest(command, input) { |
@@ -46,3 +46,3 @@ import { AccountAddress } from './types/accountAddress'; | ||
export interface TransactionEvent { | ||
tag: 'ModuleDeployed' | 'ContractInitialized' | 'AccountCreated' | 'CredentialDeployed' | 'BakerAdded' | 'BakerRemoved' | 'BakerStakeIncreased' | 'BakerStakeDecreased' | 'BakerSetRestakeEarnings' | 'BakerKeysUpdated' | 'CredentialKeysUpdated' | 'NewEncryptedAmount' | 'EncryptedAmountsRemoved' | 'AmountAddedByDecryption' | 'EncryptedSelfAmountAdded' | 'UpdateEnqueued' | 'TransferredWithSchedule' | 'CredentialsUpdated' | 'DataRegistered' | 'Interrupted' | 'Resumed' | 'BakerSetOpenStatus' | 'BakerSetMetadataURL' | 'BakerSetTransactionFeeCommission' | 'BakerSetBakingRewardCommission' | 'BakerSetFinalizationRewardCommission' | 'DelegationStakeIncreased' | 'DelegationStakeDecreased' | 'DelegationSetRestakeEarnings' | 'DelegationSetDelegationTarget' | 'DelegationAdded' | 'DelegationRemoved'; | ||
tag: 'ModuleDeployed' | 'ContractInitialized' | 'AccountCreated' | 'CredentialDeployed' | 'BakerAdded' | 'BakerRemoved' | 'BakerStakeIncreased' | 'BakerStakeDecreased' | 'BakerSetRestakeEarnings' | 'BakerKeysUpdated' | 'CredentialKeysUpdated' | 'NewEncryptedAmount' | 'EncryptedAmountsRemoved' | 'AmountAddedByDecryption' | 'EncryptedSelfAmountAdded' | 'UpdateEnqueued' | 'TransferredWithSchedule' | 'CredentialsUpdated' | 'DataRegistered' | 'BakerSetOpenStatus' | 'BakerSetMetadataURL' | 'BakerSetTransactionFeeCommission' | 'BakerSetBakingRewardCommission' | 'BakerSetFinalizationRewardCommission' | 'DelegationStakeIncreased' | 'DelegationStakeDecreased' | 'DelegationSetRestakeEarnings' | 'DelegationSetDelegationTarget' | 'DelegationAdded' | 'DelegationRemoved'; | ||
} | ||
@@ -53,2 +53,12 @@ export interface ContractAddress { | ||
} | ||
export interface InterruptedEvent { | ||
tag: 'Interrupted'; | ||
address: ContractAddress; | ||
events: string[]; | ||
} | ||
export interface ResumedEvent { | ||
tag: 'Resumed'; | ||
address: ContractAddress; | ||
success: boolean; | ||
} | ||
export interface UpdatedEvent { | ||
@@ -143,6 +153,22 @@ tag: 'Updated'; | ||
} | ||
export interface RejectReason { | ||
tag: RejectReasonTag; | ||
export interface RejectedReceive { | ||
tag: RejectReasonTag.RejectedReceive; | ||
contractAddress: ContractAddress; | ||
receiveName: string; | ||
rejectReason: number; | ||
parameter: string; | ||
} | ||
export interface RejectedInit { | ||
tag: RejectReasonTag.RejectedInit; | ||
rejectReason: number; | ||
} | ||
export declare type SimpleRejectReasonTag = RejectReasonTag.ModuleNotWF | RejectReasonTag.RuntimeFailure | RejectReasonTag.SerializationFailure | RejectReasonTag.OutOfEnergy | RejectReasonTag.InvalidProof | RejectReasonTag.InsufficientBalanceForBakerStake | RejectReasonTag.StakeUnderMinimumThresholdForBaking | RejectReasonTag.BakerInCooldown | RejectReasonTag.NonExistentCredentialID | RejectReasonTag.KeyIndexAlreadyInUse | RejectReasonTag.InvalidAccountThreshold | RejectReasonTag.InvalidCredentialKeySignThreshold | RejectReasonTag.InvalidEncryptedAmountTransferProof | RejectReasonTag.InvalidTransferToPublicProof | RejectReasonTag.InvalidIndexOnEncryptedTransfer | RejectReasonTag.ZeroScheduledAmount | RejectReasonTag.NonIncreasingSchedule | RejectReasonTag.FirstScheduledReleaseExpired | RejectReasonTag.InvalidCredentials | RejectReasonTag.RemoveFirstCredential | RejectReasonTag.CredentialHolderDidNotSign | RejectReasonTag.NotAllowedMultipleCredentials | RejectReasonTag.NotAllowedToReceiveEncrypted | RejectReasonTag.NotAllowedToHandleEncrypted | RejectReasonTag.MissingBakerAddParameters | RejectReasonTag.FinalizationRewardCommissionNotInRange | RejectReasonTag.BakingRewardCommissionNotInRange | RejectReasonTag.TransactionFeeCommissionNotInRange | RejectReasonTag.AlreadyADelegator | RejectReasonTag.InsufficientBalanceForDelegationStake | RejectReasonTag.MissingDelegationAddParameters | RejectReasonTag.InsufficientDelegationStake | RejectReasonTag.DelegatorInCooldown | RejectReasonTag.StakeOverMaximumThresholdForPool | RejectReasonTag.PoolWouldBecomeOverDelegated | RejectReasonTag.PoolClosed; | ||
export interface SimpleRejectReason { | ||
tag: SimpleRejectReasonTag; | ||
} | ||
export interface RejectReasonWithContents { | ||
tag: Exclude<RejectReasonTag, RejectReasonTag.RejectedReceive | RejectReasonTag.RejectedInit | SimpleRejectReasonTag>; | ||
contents: any; | ||
} | ||
export declare type RejectReason = RejectReasonWithContents | SimpleRejectReason | RejectedReceive | RejectedInit; | ||
interface RejectedEventResult { | ||
@@ -154,3 +180,3 @@ outcome: 'reject'; | ||
outcome: 'success'; | ||
events: (TransactionEvent | TransferredEvent | UpdatedEvent | MemoEvent | TransferredWithScheduleEvent)[]; | ||
events: (TransactionEvent | TransferredEvent | UpdatedEvent | ResumedEvent | InterruptedEvent | MemoEvent | TransferredWithScheduleEvent)[]; | ||
} | ||
@@ -978,2 +1004,21 @@ export declare type EventResult = SuccessfulEventResult | TransferWithMemoEventResult | RejectedEventResult; | ||
export declare type InstanceInfoSerialized = InstanceInfoSerializedV0 | InstanceInfoSerializedV1; | ||
export interface ContractContext { | ||
invoker?: ContractAddress | AccountAddress; | ||
contract: ContractAddress; | ||
amount?: GtuAmount; | ||
method: string; | ||
parameter?: Buffer; | ||
energy?: bigint; | ||
} | ||
export interface InvokeContractSuccessResult extends Pick<SuccessfulEventResult, 'events'> { | ||
tag: 'success'; | ||
usedEnergy: bigint; | ||
returnValue?: string; | ||
} | ||
export interface InvokeContractFailedResult { | ||
tag: 'failure'; | ||
usedEnergy: bigint; | ||
reason: RejectReason; | ||
} | ||
export declare type InvokeContractResult = InvokeContractSuccessResult | InvokeContractFailedResult; | ||
export interface CredentialDeploymentTransaction { | ||
@@ -980,0 +1025,0 @@ expiry: TransactionExpiry; |
import { AccountTransactionSignature } from './types'; | ||
export declare function intListToStringList(jsonStruct: string): string; | ||
/** | ||
* Replaces a string in a JSON string with the same string as a | ||
* number, i.e. removing quotes (") prior to and after the string. This | ||
* is needed as the default JSON stringify cannot serialize BigInts as numbers. | ||
* So one can turn them into strings, stringify the structure, and then use this function | ||
* to make those fields into JSON numbers. | ||
* @param jsonStruct the JSON structure as a string | ||
* @param keys the keys where the strings has to be unquoted | ||
* @returns the same JSON string where the strings at the supplied keys are unquoted | ||
*/ | ||
export declare function stringToInt(jsonStruct: string, keys: string[]): string; | ||
/** | ||
* A transformer that converts all the values provided as keys to | ||
@@ -5,0 +16,0 @@ * string values. |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.secondsSinceEpoch = exports.countSignatures = exports.isValidHash = exports.isHex = exports.buildJsonResponseReviver = exports.unwrapJsonResponse = exports.unwrapBoolResponse = exports.intToStringTransformer = exports.intListToStringList = void 0; | ||
exports.secondsSinceEpoch = exports.countSignatures = exports.isValidHash = exports.isHex = exports.buildJsonResponseReviver = exports.unwrapJsonResponse = exports.unwrapBoolResponse = exports.intToStringTransformer = exports.stringToInt = exports.intListToStringList = void 0; | ||
const concordium_p2p_rpc_pb_1 = require("../grpc/concordium_p2p_rpc_pb"); | ||
@@ -15,5 +15,5 @@ /** | ||
function intToString(jsonStruct, keys) { | ||
const result = jsonStruct; | ||
let result = jsonStruct; | ||
for (const key of keys) { | ||
result.replace(new RegExp(`"${key}":\\s*([0-9]+)`, 'g'), `"${key}":"$1"`); | ||
result = result.replace(new RegExp(`"${key}":\\s*([0-9]+)`, 'g'), `"${key}":"$1"`); | ||
} | ||
@@ -27,2 +27,20 @@ return result; | ||
/** | ||
* Replaces a string in a JSON string with the same string as a | ||
* number, i.e. removing quotes (") prior to and after the string. This | ||
* is needed as the default JSON stringify cannot serialize BigInts as numbers. | ||
* So one can turn them into strings, stringify the structure, and then use this function | ||
* to make those fields into JSON numbers. | ||
* @param jsonStruct the JSON structure as a string | ||
* @param keys the keys where the strings has to be unquoted | ||
* @returns the same JSON string where the strings at the supplied keys are unquoted | ||
*/ | ||
function stringToInt(jsonStruct, keys) { | ||
let result = jsonStruct; | ||
for (const key of keys) { | ||
result = result.replace(new RegExp(`"${key}":\\s*"([0-9]+)"`, 'g'), `"${key}":$1`); | ||
} | ||
return result; | ||
} | ||
exports.stringToInt = stringToInt; | ||
/** | ||
* A transformer that converts all the values provided as keys to | ||
@@ -29,0 +47,0 @@ * string values. |
{ | ||
"name": "@concordium/node-sdk", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"description": "Helpers for interacting with the Concordium node", | ||
@@ -5,0 +5,0 @@ "repository": { |
@@ -916,2 +916,42 @@ # concordium-node-sdk-js | ||
## InvokeContract | ||
Used to simulate a contract update, and to trigger view functions. | ||
```js | ||
const blockHash = "7f7409679e53875567e2ae812c9fcefe90ced8961d08554756f42bf268a42749"; | ||
const contractAddress = { index: 1n, subindex: 0n }; | ||
const invoker = new AccountAddress('3tXiu8d4CWeuC12irAB7YVb1hzp3YxsmmmNzzkdujCPqQ9EjDm'); | ||
const result = await client.invokeContract( | ||
blockHash, | ||
{ | ||
invoker: invoker, | ||
contract: contractAddress, | ||
method: 'PiggyBank.smash', | ||
amount: undefined, | ||
parameter: undefined, | ||
energy: 30000n, | ||
} | ||
); | ||
if (!result) { | ||
// The node could not attempt the invocation, most likely the contract doesn't exist. | ||
} | ||
if (result.tag === 'failure') { | ||
// Invoke was unsuccesful | ||
const rejectReason = result.reason; // Describes why the update failed; | ||
... | ||
} else { | ||
const events = result.events; // a list of events that would be generated by the update | ||
const returnValue = result.returnValue; // If the invoked method has return value | ||
... | ||
} | ||
``` | ||
Note that some of the parts of the context are optional: | ||
- amount: defaults to 0 | ||
- energy: defaults to 10 million | ||
- parameter: defaults to no parameters | ||
- invoker: uses the zero account address, which can be used instead of finding a random address. | ||
## Deserialize contract state | ||
@@ -918,0 +958,0 @@ The following example demonstrates how to deserialize a contract's state: |
2059489
0.57%12265
1.23%998
4.18%