diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 6501389802..55a1ee2d2f 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -1,12 +1,14 @@ ## master -_06/19/2023_ +_06/20/2023_ https://github.com/gear-tech/gear-js/pull/1298 +https://github.com/gear-tech/gear-js/pull/1301 ### Changes - New approach to generate program IDs +- Support `resumeSessionInit`, `resumeSessionPush` and `resumeSessionCommit` extrinsics according to https://github.com/gear-tech/gear/pull/2622 ## 0.31.2 diff --git a/api/README.md b/api/README.md index 36a9b07011..b4bde2584d 100644 --- a/api/README.md +++ b/api/README.md @@ -298,6 +298,40 @@ console.log(gas.toHuman()); ``` --- +### Resume paused program + +To resume paused program use `api.program.resumeSession` methods. +`init` - To start new session to resume program +`push` - To push a bunch of the program pages +`commit` - To finish resume session + +```javascript +const program = await api.programStorage.getProgram(programId, oneBlockBeforePauseHash); +const initTx = api.program.resumeSession.init({ + programId, + allocations: program.allocations, + codeHash: program.codeHash.toHex(), +}); + +let sessionId: HexString; +initTx.signAndSend(account, ({ events }) => { + events.forEach(({ event: { method, data }}) => { + if (method === 'ProgramResumeSessionStarted') { + sessionId = data.sessionId.toNumber(); + } + }) +}) + +const pages = await api.programStorage.getProgramPages(programId, program, oneBlockBeforePauseHash); +for (const memPage of Object.entries(page)) { + const tx = api.program.resumeSession.push({ sessionId, memoryPages: [memPage] }); + tx.signAndSend(account); +} + +const tx = api.program.resumeSession.commit({ sessionId, blockCount: 20_000 }); +tx.signAndSend(account); +``` + ## Work with programs and blockchain state ### Check that the address belongs to some program diff --git a/api/programs/Cargo.lock b/api/programs/Cargo.lock index dc31556620..b1a4962aac 100644 --- a/api/programs/Cargo.lock +++ b/api/programs/Cargo.lock @@ -344,16 +344,16 @@ dependencies = [ [[package]] name = "galloc" -version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +version = "0.2.0" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "dlmalloc", ] [[package]] name = "gcore" -version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +version = "0.2.0" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "gear-core-errors", "gsys", @@ -364,7 +364,7 @@ dependencies = [ [[package]] name = "gear-core" version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "blake2-rfc", "derive_more", @@ -383,7 +383,7 @@ dependencies = [ [[package]] name = "gear-core-errors" version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "derive_more", "enum-iterator", @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "gear-wasm-builder" version = "0.1.2" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "anyhow", "cargo_metadata", @@ -416,7 +416,7 @@ dependencies = [ [[package]] name = "gear-wasm-instrument" version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "enum-iterator", "wasm-instrument", @@ -424,8 +424,8 @@ dependencies = [ [[package]] name = "gmeta" -version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +version = "0.2.0" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "blake2-rfc", "derive_more", @@ -437,7 +437,7 @@ dependencies = [ [[package]] name = "gmeta-codegen" version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "proc-macro2", "quote", @@ -446,8 +446,8 @@ dependencies = [ [[package]] name = "gstd" -version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +version = "0.2.0" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "bs58", "futures", @@ -466,7 +466,7 @@ dependencies = [ [[package]] name = "gstd-codegen" version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" dependencies = [ "proc-macro2", "quote", @@ -475,8 +475,8 @@ dependencies = [ [[package]] name = "gsys" -version = "0.1.0" -source = "git+https://github.com/gear-tech/gear.git#82c8fc6a4b96fe3d256ae0b1bc33c95ead4ece1e" +version = "0.2.0" +source = "git+https://github.com/gear-tech/gear.git#fe847046d3909871c2252a7a22794e3bcecb608e" [[package]] name = "hashbrown" @@ -647,9 +647,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430d26d62e66cbff6ae144e8eebd43c0235922dec7e3aa0d2016c32d4575bf45" +checksum = "2287753623c76f953acd29d15d8100bcab84d29db78fb6f352adb3c53e83b967" dependencies = [ "arrayvec 0.7.4", "bitvec", @@ -661,9 +661,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1620b1e3fc72ebaee01ff56fca838bab537c5d093a18b3549c3bbea374aa0b6" +checksum = "2b6937b5e67bfba3351b87b040d48352a2fcb6ad72f81855412ce97b45c8f110" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -798,9 +798,9 @@ checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "scale-info" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b569c32c806ec3abdf3b5869fb8bf1e0d275a7c1c9b0b05603d9464632649edf" +checksum = "ad560913365790f17cbf12479491169f01b9d46d29cfc7422bf8c64bdc61b731" dependencies = [ "cfg-if", "derive_more", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53012eae69e5aa5c14671942a5dd47de59d4cdcff8532a6dd0e081faf1119482" +checksum = "19df9bd9ace6cc2fe19387c96ce677e823e07d017ceed253e7bb3d1d1bd9c73b" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/api/src/Message.ts b/api/src/Message.ts index af81a5d23c..36f158fda3 100644 --- a/api/src/Message.ts +++ b/api/src/Message.ts @@ -16,7 +16,7 @@ export class GearMessage extends GearTransaction { * @param args Message parameters * @param meta Program metadata obtained using `getProgramMetadata` function. * @param typeIndex (optional) Index of type in the registry. If not specified the type index from `meta.handle.input` will be used instead. - * @returns Submitted result + * @returns Submittable result * ```javascript * const programId = '0x..'; * const hexMeta = '0x...'; diff --git a/api/src/Program.ts b/api/src/Program.ts index cf765688e7..594c725a35 100644 --- a/api/src/Program.ts +++ b/api/src/Program.ts @@ -24,15 +24,18 @@ import { } from './utils'; import { GearApi } from './GearApi'; import { GearGas } from './Gas'; +import { GearResumeSession } from './ResumeSession'; import { GearTransaction } from './Transaction'; import { ProgramMetadata } from './metadata'; export class GearProgram extends GearTransaction { public calculateGas: GearGas; + public resumeSession: GearResumeSession; constructor(protected _api: GearApi) { super(_api); this.calculateGas = new GearGas(_api); + this.resumeSession = new GearResumeSession(_api); } /** diff --git a/api/src/ResumeSession.ts b/api/src/ResumeSession.ts new file mode 100644 index 0000000000..b69a058c74 --- /dev/null +++ b/api/src/ResumeSession.ts @@ -0,0 +1,120 @@ +import { hexToU8a, u8aToHex } from '@polkadot/util'; +import { ISubmittableResult } from '@polkadot/types/types'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; + +import { IResumeSessionCommitArgs, IResumeSessionInitArgs, IResumeSessionPushArgs } from './types'; +import { ResumeSessionCommitError, ResumeSessionInitError, ResumeSessionPushError } from './errors'; +import { CreateType } from './metadata'; +import { GearApi } from './GearApi'; +import { GearTransaction } from './Transaction'; + +const SIXTEEN_KB = 16384; + +export class GearResumeSession extends GearTransaction { + constructor(protected _api: GearApi) { + super(_api); + } + + /** + * ## Create a session for program resume. Get session id from `ProgramResumeSessionStarted` event + * @param args Resume program args + * @returns Submittable result + * @example + * ```javascript + * const program = await api.programStorage.getProgram(programId, oneBlockBeforePauseHash); + * const initTx = api.program.resumeSession.init({ + * programId, + * allocations: program.allocations, + * codeHash: program.codeHash.toHex(), + * }); + * + * let sessionId: HexString; + * initTx.signAndSend(account, ({ events }) => { + * events.forEach(({ event: { method, data }}) => { + * if (method === 'ProgramResumeSessionStarted') { + * sessionId = data.sessionId.toNumber(); + * } + * }) + * }) + * ``` + */ + init({ + programId, + allocations, + codeHash, + }: IResumeSessionInitArgs): SubmittableExtrinsic<'promise', ISubmittableResult> { + try { + this.extrinsic = this._api.tx.gear.resumeSessionInit( + programId, + Array.from(CreateType.create('BTreeSet', allocations).toU8a()), + codeHash, + ); + return this.extrinsic; + } catch (error) { + console.log(error); + throw new ResumeSessionInitError(programId, error.message); + } + } + + /** + * ## Append program memory pages to the session data. + * @param args Push pages args + * @returns Submittable result + * @example + * ```javascript + * const pages = await api.programStorage.getProgramPages(programId, program, oneBlockBeforePauseHash); + * for (const memPage of Object.entries(page)) { + * const tx = api.program.resumeSession.push({ sessionId, memoryPages: [memPage] }); + * tx.signAndSend(account); + * } + * ``` + */ + push({ sessionId, memoryPages }: IResumeSessionPushArgs): SubmittableExtrinsic<'promise', ISubmittableResult> { + if ( + !memoryPages.every(([_, page]) => { + if (typeof page === 'string') { + return page.length === SIXTEEN_KB * 2 + 2; + } else { + return page.length === SIXTEEN_KB; + } + }) + ) { + throw new ResumeSessionPushError(sessionId, 'Invalid memory page length. Must be 16KB.'); + } + + const vecLen = CreateType.create('Compact', memoryPages.length).toHex(); + + const tuples = memoryPages.map(([number, page]) => { + const num = CreateType.create('u32', number).toHex(); + const p = typeof page === 'string' ? page : u8aToHex(page); + return num + p.slice(2); + }); + + try { + this.extrinsic = this._api.tx.gear.resumeSessionPush(sessionId, vecLen + tuples.slice(2)); + return this.extrinsic; + } catch (error) { + throw new ResumeSessionPushError(sessionId); + } + } + + /** + * ## Finish program resume session with the given key `sessionId`. + * @param args Commit session args + * @returns Submittable result + * @example + * ```javascript + * const tx = api.program.resumeSession.commit({ sessionId, blockCount: 20_000 }); + * tx.signAndSend(account); + * ``` + */ + commit({ sessionId, blockCount }: IResumeSessionCommitArgs): SubmittableExtrinsic<'promise', ISubmittableResult> { + try { + this.extrinsic = this._api.tx.gear.resumeSessionCommit(sessionId, blockCount); + return this.extrinsic; + } catch (error) { + console.log(error); + throw new ResumeSessionCommitError(sessionId, error.message); + } + } +} diff --git a/api/src/Storage.ts b/api/src/Storage.ts index 3b2233323b..31032f3f3e 100644 --- a/api/src/Storage.ts +++ b/api/src/Storage.ts @@ -21,7 +21,8 @@ export class GearProgramStorage { * @returns */ async getProgram(id: HexString, at?: HexString): Promise { - const programOption = (await this._api.query.gearProgram.programStorage(id, at)) as Option; + const api = at ? await this._api.at(at) : this._api; + const programOption = (await api.query.gearProgram.programStorage(id)) as Option; if (programOption.isNone) { throw new ProgramDoesNotExistError(id); @@ -42,12 +43,13 @@ export class GearProgramStorage { * @param gProg * @returns */ - async getProgramPages(programId: HexString, program: ActiveProgram): Promise { + async getProgramPages(programId: HexString, program: ActiveProgram, at?: HexString): Promise { const pages = {}; for (const page of program.pagesWithData) { pages[page.toNumber()] = u8aToU8a( await this._api.provider.send('state_getStorage', [ this._api.query.gearProgram.memoryPageStorage.key(programId, page), + at, ]), ); } diff --git a/api/src/errors/program.errors.ts b/api/src/errors/program.errors.ts index 36ae98b8a6..46fb5dcfd5 100644 --- a/api/src/errors/program.errors.ts +++ b/api/src/errors/program.errors.ts @@ -61,3 +61,27 @@ export class ProgramHasNoMetahash extends Error { super(`Program with id ${id} has not metahash function`); } } + +export class ResumeSessionInitError extends Error { + name = 'ResumeSessionInitError'; + + constructor(programId: string, msg?: string) { + super(`Resume session init for program ${programId} failed. ${msg ? msg : ''}`); + } +} + +export class ResumeSessionPushError extends Error { + name = 'ResumeSessionPushError'; + + constructor(sessionId: string | number | bigint, msg?: string) { + super(`Failed to push pages to session ${sessionId}. ${msg || ''}`); + } +} + +export class ResumeSessionCommitError extends Error { + name = 'ResumeSessionCommitError'; + + constructor(sessionId: string | number | bigint, msg?: string) { + super(`Failed to commit session ${sessionId}. ${msg || ''}`); + } +} diff --git a/api/src/events/GearEventData.ts b/api/src/events/GearEventData.ts index cb9d66d122..3a8a226bdb 100644 --- a/api/src/events/GearEventData.ts +++ b/api/src/events/GearEventData.ts @@ -12,6 +12,7 @@ import { MessageWokenReason, ProgramChangedKind, ProgramId, + ResumeProgramSessionId, UserMessageReadReason, UserMessageSentMessage, } from '../types'; @@ -82,3 +83,10 @@ export interface TransferData extends GenericEventData { to: AccountId32; amount: u128; } + +export interface ProgramResumeSessionStartedData extends GenericEventData { + sessionId: ResumeProgramSessionId; + accountId: AccountId32; + programId: ProgramId; + sessionEndBlock: BlockNumber; +} diff --git a/api/src/events/GearEvents.ts b/api/src/events/GearEvents.ts index e4401be33d..88e92d132b 100644 --- a/api/src/events/GearEvents.ts +++ b/api/src/events/GearEvents.ts @@ -9,6 +9,7 @@ import { MessageWakenData, MessagesDispatchedData, ProgramChangedData, + ProgramResumeSessionStartedData, TransferData, UserMessageReadData, UserMessageSentData, @@ -39,3 +40,5 @@ export type DebugDataSnapshot = GearEvent; export type DebugMode = GearEvent; export type Transfer = GearEvent; + +export type ProgramResumeSessionStarted = GearEvent; diff --git a/api/src/events/types.ts b/api/src/events/types.ts index 0ade0069ad..10ad79f60a 100644 --- a/api/src/events/types.ts +++ b/api/src/events/types.ts @@ -7,6 +7,7 @@ import { MessageWaken, MessagesDispatched, ProgramChanged, + ProgramResumeSessionStarted, UserMessageRead, UserMessageSent, } from './GearEvents'; @@ -22,4 +23,5 @@ export interface IGearEvent { ProgramChanged: ProgramChanged; DebugDataSnapshot: DebugDataSnapshot; DebugMode: DebugMode; + ProgramResumeSessionStarted: ProgramResumeSessionStarted; } diff --git a/api/src/types/interfaces/ids/program.ts b/api/src/types/interfaces/ids/program.ts index 38ec69c72f..e136a6ea67 100644 --- a/api/src/types/interfaces/ids/program.ts +++ b/api/src/types/interfaces/ids/program.ts @@ -1,3 +1,6 @@ import { Hash } from '@polkadot/types/interfaces'; +import { u128 } from '@polkadot/types'; export type ProgramId = Hash; + +export type ResumeProgramSessionId = u128; diff --git a/api/src/types/interfaces/program/extrinsic.ts b/api/src/types/interfaces/program/extrinsic.ts index b0d9593fed..6c5a3de35e 100644 --- a/api/src/types/interfaces/program/extrinsic.ts +++ b/api/src/types/interfaces/program/extrinsic.ts @@ -1,3 +1,4 @@ +import { BTreeSet, u32 } from '@polkadot/types'; import { AnyJson } from '@polkadot/types/types'; import { HexString } from '@polkadot/util/types'; import { ISubmittableResult } from '@polkadot/types/types'; @@ -25,3 +26,42 @@ export interface IProgramUploadResult { } export type IProgramCreateResult = Omit; + +export interface IResumeSessionInitArgs { + /** + * Program ID to resume + */ + programId: HexString; + /** + * Allocations obtained with `api.programStorage.getProgramPages` method + */ + allocations: Array | BTreeSet | HexString; + /** + * Hash of the code of the program + */ + codeHash: HexString; +} + +export type GearPageNumberHuman = string | number | bigint; + +export interface IResumeSessionPushArgs { + /** + * Session ID recieved during `resumeSessionInit` transaction + */ + sessionId: string | number | bigint; + /** + * Program pages with data + */ + memoryPages: Array<[GearPageNumberHuman, HexString | Uint8Array]>; +} + +export interface IResumeSessionCommitArgs { + /** + * Session ID recieved during `resumeSessionInit` transaction + */ + sessionId: string | number | bigint; + /** + * Count of blocks till program will be paused + */ + blockCount: string | number | bigint; +} diff --git a/api/src/types/interfaces/program/pages.ts b/api/src/types/interfaces/program/pages.ts index 0fa293bfb1..e5dd0015f1 100644 --- a/api/src/types/interfaces/program/pages.ts +++ b/api/src/types/interfaces/program/pages.ts @@ -5,3 +5,5 @@ export interface IGearPages { } export type WasmPageNumber = u32; + +export type GearPageNumber = u32; diff --git a/api/test/Code.test.ts b/api/test/Code.test.ts index e2889c86b7..204a5665e4 100644 --- a/api/test/Code.test.ts +++ b/api/test/Code.test.ts @@ -26,10 +26,10 @@ describe('Upload code', () => { const { codeHash } = await api.code.upload(code); expect(codeHash).toBeDefined(); codeId = codeHash; - const transactionData = await sendTransaction(api.code, accounts['alice'], 'CodeChanged'); - expect(transactionData.id).toBe(codeHash); - expect(transactionData.change).toHaveProperty('Active'); - expect(transactionData.change.Active).toHaveProperty('expiration'); + const [txData] = await sendTransaction(api.code, accounts['alice'], ['CodeChanged']); + expect(txData.id.toHex()).toBe(codeHash); + expect(txData.change.isActive).toBeTruthy(); + expect(txData.change.asActive).toHaveProperty('expiration'); }); test('Throw error when code exists', async () => { diff --git a/api/test/DebugMode.test.ts b/api/test/DebugMode.test.ts index 9165195e5b..95ae5a2dd0 100644 --- a/api/test/DebugMode.test.ts +++ b/api/test/DebugMode.test.ts @@ -24,7 +24,7 @@ describe.skip('DebugMode', () => { test('enable debug mode', async () => { debug.enable(); - const transactionData = await sendTransaction(debug.enabled, alice, 'DebugMode'); + const [transactionData] = await sendTransaction(debug.enabled, alice, ['DebugMode']); expect(transactionData[0]).toBeTruthy(); }); @@ -37,9 +37,9 @@ describe.skip('DebugMode', () => { code: readFileSync(join(GEAR_EXAMPLES_WASM_DIR, 'demo_ping.opt.wasm')), gasLimit: 2_000_000_000, }); - await sendTransaction(api.program.extrinsic, alice, 'MessageQueued'); + await sendTransaction(api.program.extrinsic, alice, ['MessageQueued']); api.message.send({ destination: programId, payload: 'PING', gasLimit: 2_000_000_000 }); - await sendTransaction(api.message.extrinsic, alice, 'MessageQueued'); + await sendTransaction(api.message.extrinsic, alice, ['MessageQueued']); (await unsub)(); expect(snapshots).toHaveLength(2); for (const snapshot of snapshots) { @@ -56,7 +56,7 @@ describe.skip('DebugMode', () => { test('disable debug mode', async () => { debug.disable(); - const transactionData = await sendTransaction(debug.enabled, alice, 'DebugMode'); + const [transactionData] = await sendTransaction(debug.enabled, alice, ['DebugMode']); expect(transactionData[0]).toBeFalsy(); }); }); diff --git a/api/test/Gas.test.ts b/api/test/Gas.test.ts index 5c7a397639..9b80bd2195 100644 --- a/api/test/Gas.test.ts +++ b/api/test/Gas.test.ts @@ -57,7 +57,7 @@ describe('Calculate gas', () => { programId = program.programId; codeId = program.codeId; const initStatus = checkInit(api, programId); - await sendTransaction(program.extrinsic, alice, 'MessageQueued'); + await sendTransaction(program.extrinsic, alice, ['MessageQueued']); expect(await initStatus).toBe('success'); }); @@ -81,7 +81,7 @@ describe('Calculate gas', () => { ); programId = program.programId; const initStatus = checkInit(api, programId); - await sendTransaction(program.extrinsic, alice, 'MessageQueued'); + await sendTransaction(program.extrinsic, alice, ['MessageQueued']); expect(await initStatus).toBe('success'); }); @@ -110,7 +110,7 @@ describe('Calculate gas', () => { meta, ); const waitForReply = listenToUserMessageSent(api, programId); - await sendTransaction(tx, alice, 'MessageQueued'); + await sendTransaction(tx, alice, ['MessageQueued']); const { message } = await waitForReply(null); expect(message.id).toBeDefined(); messageId = message.id.toHex(); @@ -140,7 +140,7 @@ describe('Calculate gas', () => { }, meta, ); - const data = await sendTransaction(tx, alice, 'MessageQueued'); + const [data] = await sendTransaction(tx, alice, ['MessageQueued']); expect(data).toBeDefined(); }); }); diff --git a/api/test/Message.test.ts b/api/test/Message.test.ts index dd58ab4103..0fdc562713 100644 --- a/api/test/Message.test.ts +++ b/api/test/Message.test.ts @@ -38,8 +38,8 @@ describe('Gear Message', () => { metadata, ).programId; const status = checkInit(api, programId); - const transactionData = await sendTransaction(api.program, alice, 'MessageQueued'); - expect(transactionData.destination).toBe(programId); + const [txData] = await sendTransaction(api.program, alice, ['MessageQueued']); + expect(txData.destination.toHex()).toBe(programId); expect(await status).toBe('success'); }); @@ -68,10 +68,10 @@ describe('Gear Message', () => { const waitForReply = api.message.listenToReplies(programId); - const transactionData = await sendTransaction(tx, alice, 'MessageQueued'); - expect(transactionData).toBeDefined(); + const [txData] = await sendTransaction(tx, alice, ['MessageQueued']); + expect(txData).toBeDefined(); - const reply = await waitForReply(transactionData.id); + const reply = await waitForReply(txData.id.toHex()); expect(reply?.message.details.isSome).toBeTruthy(); expect(reply?.message.details.unwrap().isReply).toBeTruthy(); expect(reply?.message.details.unwrap().asReply.statusCode.toNumber()).toBe(0); @@ -104,8 +104,8 @@ describe('Gear Message', () => { test('Claim value from mailbox', async () => { expect(messageToClaim).toBeDefined(); const submitted = api.claimValueFromMailbox.submit(messageToClaim); - const transactionData = await sendTransaction(submitted, alice, 'UserMessageRead'); - expect(transactionData.id).toBe(messageToClaim); + const [txData] = await sendTransaction(submitted, alice, ['UserMessageRead']); + expect(txData.id.toHex()).toBe(messageToClaim); const mailbox = await api.mailbox.read(decodeAddress(alice.address)); expect(mailbox.filter((value) => value[0][1] === messageToClaim)).toHaveLength(0); }); diff --git a/api/test/Program.test.ts b/api/test/Program.test.ts index ab53b02600..079ad0164e 100644 --- a/api/test/Program.test.ts +++ b/api/test/Program.test.ts @@ -5,9 +5,9 @@ import { bufferToU8a } from '@polkadot/util'; import { join } from 'path'; import { readFileSync } from 'fs'; -import { GearApi, getProgramMetadata } from '../src'; +import { GearApi, decodeAddress, getProgramMetadata } from '../src'; import { TARGET, TEST_META_META, WS_ADDRESS } from './config'; -import { checkInit, getAccount, sendTransaction, sleep } from './utilsFunctions'; +import { checkInit, getAccount, sendTransaction, sleep, waitForPausedProgram } from './utilsFunctions'; const api = new GearApi({ providerAddress: WS_ADDRESS }); let alice: KeyringPair; @@ -15,6 +15,8 @@ let codeId: HexString; let programId: HexString; let expiration: number; let metaHash: HexString; +let expiredBN: number; +let pausedBlockHash: HexString; const code = readFileSync(join(TARGET, 'test_meta.opt.wasm')); const metaHex: HexString = `0x${readFileSync(TEST_META_META, 'utf-8')}`; @@ -64,12 +66,16 @@ describe('New Program', () => { const waitForReply = api.message.listenToReplies(programId); - const transactionData = await sendTransaction(program.extrinsic, alice, 'MessageQueued'); + const [pcData, mqData] = await sendTransaction(program.extrinsic, alice, ['ProgramChanged', 'MessageQueued']); + + expect(pcData.id.toHex()).toBe(programId); + expect(pcData.change.isProgramSet).toBeTruthy(); + expect(pcData.change.asProgramSet.expiration.toNumber()).toBeGreaterThan(0); + expiredBN = pcData.change.asProgramSet.expiration.toNumber(); - expect(transactionData.destination).toBe(program.programId); expect(await status).toBe('success'); - const reply = await waitForReply(transactionData.id); + const reply = await waitForReply(mqData.id.toHex()); expect(metadata.createType(metadata.types.init.output!, reply.message.payload).toJSON()).toMatchObject({ One: 1 }); expect(isProgramSetHappened).toBeTruthy(); expect(isActiveHappened).toBeTruthy(); @@ -77,6 +83,13 @@ describe('New Program', () => { expiration = activeExpiration!; }); + test.skip('Wait when program will be paused', async () => { + const [id, blockHash] = await waitForPausedProgram(api, programId, expiredBN); + expect(id).toBe(programId); + expect(blockHash).toBeDefined(); + pausedBlockHash = blockHash; + }); + test('Сreate program', async () => { expect(codeId).toBeDefined(); const metadata = getProgramMetadata(metaHex); @@ -102,15 +115,15 @@ describe('New Program', () => { const waitForReply = api.message.listenToReplies(programId); - const transactionData = await sendTransaction(api.program, alice, 'MessageQueued'); + const [transactionData] = await sendTransaction(api.program, alice, ['MessageQueued']); - expect(transactionData.destination).toBe(programId); + expect(transactionData.destination.toHex()).toBe(programId); expect(await status).toBe('success'); expect(programChangedStatuses).toContain('ProgramSet'); expect(programChangedStatuses).toContain('Active'); - const reply = await waitForReply(transactionData.id); + const reply = await waitForReply(transactionData.id.toHex()); expect(metadata.createType(metadata.types.init.output!, reply.message.payload).toJSON()).toMatchObject({ One: 1 }); }); @@ -138,11 +151,12 @@ describe('New Program', () => { test('Pay program rent', async () => { const tx = await api.program.payRent(programId, 10_000); - const result = await sendTransaction(tx, alice, 'ProgramChanged'); + const [result] = await sendTransaction(tx, alice, ['ProgramChanged']); expect(result).toHaveProperty('id'); - expect(result.id).toBe(programId); - expect(result).toHaveProperty(['change', 'ExpirationChanged', 'expiration']); - expect(Number(result.change.ExpirationChanged.expiration.replaceAll(',', ''))).toBe(expiration + 10_000); + expect(result.id.toHex()).toBe(programId); + expect(result.change.isExpirationChanged).toBeTruthy(); + expect(result.change.asExpirationChanged.expiration).toBeDefined(); + expect(Number(result.change.asExpirationChanged.expiration.toNumber())).toBe(expiration + 10_000); }); test('Calculate pay rent', () => { @@ -208,4 +222,58 @@ describe('Program', () => { const pages = await api.programStorage.getProgramPages(programId, program); expect(Object.keys(pages)).not.toHaveLength(0); }); + + test.skip('Resume program', async () => { + expect(programId).toBeDefined(); + expect(pausedBlockHash).toBeDefined(); + + const parentBlock = (await api.blocks.get(pausedBlockHash)).block.header.parentHash.toHex(); + + const program = await api.programStorage.getProgram(programId, parentBlock); + + const initTx = api.program.resumeSession.init({ + programId, + allocations: program.allocations, + codeHash: program.codeHash.toHex(), + }); + + const [txData] = await sendTransaction(initTx, alice, ['ProgramResumeSessionStarted']); + + expect(txData.sessionId).toBeDefined(); + expect(txData.accountId).toBeDefined(); + expect(txData.accountId.toHex()).toBe(decodeAddress(alice.address)); + expect(txData.programId).toBeDefined(); + expect(txData.programId.toHex()).toBe(programId); + expect(txData.sessionEndBlock).toBeDefined(); + + const sessionId = txData.sessionId.toNumber(); + + const pages = await api.programStorage.getProgramPages(programId, program, parentBlock); + + const memoryPages = Object.entries(pages); + + const txs: any = []; + + for (const memPage of memoryPages) { + txs.push(api.program.resumeSession.push({ sessionId, memoryPages: [memPage] })); + } + + await new Promise((resolve) => + api.tx.utility.batchAll(txs).signAndSend(alice, ({ events }) => { + events.forEach(({ event: { method } }) => { + if (method === 'BatchCompleted') { + resolve(true); + } + }); + }), + ); + + await new Promise((resolve) => + api.program.resumeSession.commit({ sessionId, blockCount: 20_000 }).signAndSend(alice, ({ status }) => { + if (status.isFinalized) { + resolve(true); + } + }), + ); + }); }); diff --git a/api/test/Waitlist.test.ts b/api/test/Waitlist.test.ts index c8abb94b89..61b6ed5895 100644 --- a/api/test/Waitlist.test.ts +++ b/api/test/Waitlist.test.ts @@ -21,7 +21,7 @@ beforeAll(async () => { alice = (await getAccount())[0]; programId = api.program.upload({ code, gasLimit: 20_000_000_000 }).programId; const init = checkInit(api, programId); - await sendTransaction(api.program, alice, 'MessageQueued'); + await sendTransaction(api.program, alice, ['MessageQueued']); expect(await init).toBe('success'); messageWaited = listenToMessageWaited(api); }); @@ -34,7 +34,7 @@ afterAll(async () => { describe('GearWaitlist', () => { test("read program's waitlist", async () => { api.message.send({ destination: programId, payload: '0x00', gasLimit: 20_000_000_000 }); - messageId = (await sendTransaction(api.message, alice, 'MessageQueued')).id; + messageId = (await sendTransaction(api.message, alice, ['MessageQueued']))[0].id.toHex(); const eventData = await messageWaited(messageId); expect(eventData).toBeDefined(); expect(eventData).toHaveProperty('reason'); @@ -65,7 +65,7 @@ describe('GearWaitlist', () => { test("send one more message and read program's waitlist", async () => { api.message.send({ destination: programId, payload: '0x00', gasLimit: 20_000_000_000 }); - messageId = (await sendTransaction(api.message, alice, 'MessageQueued'))[0]; + messageId = (await sendTransaction(api.message, alice, ['MessageQueued']))[0]; const waitlist = await api.waitlist.read(programId); expect(waitlist).toHaveLength(2); }); diff --git a/api/test/utilsFunctions.ts b/api/test/utilsFunctions.ts index e9fb79b848..36ba54582f 100644 --- a/api/test/utilsFunctions.ts +++ b/api/test/utilsFunctions.ts @@ -8,6 +8,7 @@ import { GearTransaction, IGearEvent, MessageWaitedData, + ProgramChangedData, UserMessageSent, UserMessageSentData, } from '../src'; @@ -76,18 +77,22 @@ export function listenToUserMessageSent(api: GearApi, programId: HexString) { export async function sendTransaction( submitted: GearTransaction | SubmittableExtrinsic<'promise'>, account: KeyringPair, - methodName: E, + methods: E[], ): Promise { + const result: any = new Array(methods.length); return new Promise((resolve, reject) => { submitted .signAndSend(account, ({ events, status }) => { events.forEach(({ event: { method, data } }) => { - if (method === methodName && status.isFinalized) { - resolve(data.toHuman()); + if (methods.includes(method as E) && status.isInBlock) { + result[methods.indexOf(method as E)] = data; } else if (method === 'ExtrinsicFailed') { reject(data.toString()); } }); + if (status.isInBlock) { + resolve(result); + } }) .catch((err) => { console.log(err); @@ -120,3 +125,24 @@ export const listenToMessageWaited = (api: GearApi) => { return message; }; }; + +export const waitForPausedProgram = ( + api: GearApi, + programId: HexString, + blockNumber: number, +): Promise<[HexString, HexString]> => { + return new Promise((resolve) => { + const unsub = api.derive.chain.subscribeNewBlocks(({ block, events }) => { + if (block.header.number.eq(blockNumber)) { + const event = events.filter( + ({ event: { method, data } }) => + method === 'ProgramChanged' && + (data as ProgramChangedData).id.eq(programId) && + (data as ProgramChangedData).change.isPaused, + ); + unsub.then((fn) => fn()); + resolve([(event[0].event.data as ProgramChangedData).id.toHex(), block.header.hash.toHex()]); + } + }); + }); +};