Skip to content

Commit

Permalink
Migration example (#352)
Browse files Browse the repository at this point in the history
state migration
  • Loading branch information
volovyks authored Mar 7, 2023
1 parent dd27277 commit 755e2fd
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 8 deletions.
38 changes: 38 additions & 0 deletions examples/__tests__/test-state-migration.ava.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Worker } from "near-workspaces";
import test from "ava";

test.beforeEach(async (t) => {
const worker = await Worker.init();
const root = worker.rootAccount;
const contract = await root.devDeploy("./build/state-migration-original.wasm");

const ali = await root.createSubAccount("ali");

t.context.worker = worker;
t.context.accounts = { root, contract, ali };
});

test.afterEach.always(async (t) => {
await t.context.worker.tearDown().catch((error) => {
console.log("Failed to tear down the worker:", error);
});
});

test("migration works", async (t) => {
const { contract, ali } = t.context.accounts;

await ali.call(contract, "addMessage", { message: { sender: "ali", header: "h1", text: "hello" } });
await ali.call(contract, "addMessage", { message: { sender: "ali", header: "h2", text: "world" } });
await ali.call(contract, "addMessage", { message: { sender: "ali", header: "h3", text: "This message is too log for new standard" } });
await ali.call(contract, "addMessage", { message: { sender: "ali", header: "h4", text: "!" } });

const res1 = await contract.view("countMessages", {});
t.is(res1, 4);

contract.deploy("./build/state-migration-new.wasm");

await ali.call(contract, "migrateState", {});

const res2 = await contract.view("countMessages", {});
t.is(res2, 3);
});
4 changes: 4 additions & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"build:nft-receiver": "near-sdk-js build src/standard-nft/test-token-receiver.ts build/nft-receiver.wasm",
"build:nft-approval-receiver": "near-sdk-js build src/standard-nft/test-approval-receiver.ts build/nft-approval-receiver.wasm",
"build:ft": "near-sdk-js build src/standard-ft/my-ft.ts build/my-ft.wasm",
"build:state-migration": "run-s build:state-migration:*",
"build:state-migration:original": "near-sdk-js build src/state-migration-original.ts build/state-migration-original.wasm",
"build:state-migration:new": "near-sdk-js build src/state-migration-new.ts build/state-migration-new.wasm",
"test": "ava && pnpm test:counter-lowlevel && pnpm test:counter-ts",
"test:nft": "ava __tests__/standard-nft/*",
"test:ft": "ava __tests__/standard-ft/*",
Expand All @@ -41,6 +44,7 @@
"test:status-message-collections": "ava __tests__/test-status-message-collections.ava.js",
"test:parking-lot": "ava __tests__/test-parking-lot.ava.js",
"test:programmatic-update": "ava __tests__/test-programmatic-update.ava.js",
"test:state-migration": "ava __tests__/test-state-migration.ava.js",
"test:nested-collections": "ava __tests__/test-nested-collections.ava.js"
},
"author": "Near Inc <hello@nearprotocol.com>",
Expand Down
71 changes: 71 additions & 0 deletions examples/src/state-migration-new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NearBindgen, view, near, migrate, call, Vector, assert } from 'near-sdk-js'
import { AccountId } from 'near-sdk-js/lib/types'

type OldMessageFormat = {
sender: AccountId,
header: string,
text: string,
}

// This is the new version of the Message type, it has an additional field
type NewMessageFormat = {
sender: AccountId,
recipient?: AccountId,
header: string,
text: string,
}

@NearBindgen({})
export class MigrationDemo {
messages: Vector<NewMessageFormat>;

constructor() {
this.messages = new Vector<NewMessageFormat>('messages');
}

@call({ payableFunction: true })
addMessage({ message }: {
message: NewMessageFormat
}): void {
this.messages.push(message);
near.log(`${near.signerAccountId()} added message ${JSON.stringify(message)}`);
}

@view({})
countMessages(): number {
return this.messages.toArray().length;
}


@migrate({})
migrateState(): Vector<NewMessageFormat> {
assert(this.messages.toArray().length == 0, "Contract state should not be deserialized in @migrate");
// retrieve the current state from the contract
let raw_vector = JSON.parse(near.storageRead("STATE")).messages;
let old_messages: Vector<OldMessageFormat> = new Vector<OldMessageFormat>(raw_vector.prefix, raw_vector.length);
near.log("old_messages: " + JSON.stringify(old_messages));

// iterate through the state migrating it to the new version
let new_messages: Vector<NewMessageFormat> = new Vector('messages');

for (let old_message of old_messages) {
near.log(`migrating ${JSON.stringify(old_message)}`);
const new_message: NewMessageFormat = {
sender: old_message.sender,
recipient: "Unknown",
header: old_message.header,
text: old_message.text,
};
if (new_message.text.length < 10) {
near.log(`adding ${new_message} to new_messages`);
new_messages.push(new_message);
} else {
near.log(`${new_message} is too long, skipping`);
}
}

this.messages = new_messages;

return this.messages;
}
}
30 changes: 30 additions & 0 deletions examples/src/state-migration-original.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NearBindgen, view, near, migrate, call, Vector, assert } from 'near-sdk-js'
import { AccountId } from 'near-sdk-js/lib/types'

type Message = {
sender: AccountId,
header: string,
text: string,
}

@NearBindgen({})
export class MigrationDemo {
messages: Vector<Message>;

constructor() {
this.messages = new Vector<Message>('messages');
}

@call({ payableFunction: true })
addMessage({ message }: {
message: Message
}): void {
this.messages.push(message);
near.log(`${near.signerAccountId()} added message ${JSON.stringify(message)}`);
}

@view({})
countMessages(): number {
return this.messages.toArray().length;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/near-sdk-js/lib/near-bindgen.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/near-sdk-js/lib/near-bindgen.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions packages/near-sdk-js/src/cli/build-tools/near-bindgen-exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { Signale } = signal;
/**
* A list of supported method types/decorators.
*/
const methodTypes = ["call", "view", "initialize"];
const methodTypes = ["call", "view", "initialize", "migrate"];

/**
* A helper function that inserts a new throw Error statement with
Expand All @@ -34,7 +34,16 @@ function throwError(message: string): t.BlockStatement {
*
* @param classId - The class ID of the class which we are extending.
*/
function readState(classId: t.Identifier): t.VariableDeclaration {
function readState(classId: t.Identifier, methodType: string): t.VariableDeclaration {
if (methodType === "migrate") {
return t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("_state"),
t.nullLiteral()
),
]);
}

return t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("_state"),
Expand Down Expand Up @@ -216,7 +225,7 @@ function saveToStorage(
classId: t.Identifier,
methodType: string
): t.EmptyStatement | t.ExpressionStatement {
if (!["initialize", "call"].includes(methodType)) {
if (!["initialize", "call", "migrate"].includes(methodType)) {
return t.emptyStatement();
}

Expand Down Expand Up @@ -315,7 +324,7 @@ function createDeclaration(
t.blockStatement([
// Read the state of the contract from storage.
// const _state = Contract._getState();
readState(classId),
readState(classId, methodType),
// Throw if initialized on any subsequent init function calls.
// if (_state) { throw new Error('Contract already initialized'); }
preventDoubleInit(methodType),
Expand Down
15 changes: 15 additions & 0 deletions packages/near-sdk-js/src/near-bindgen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ type DecoratorFunction = <AnyFunction extends (...args: any) => any>(
descriptor: TypedPropertyDescriptor<AnyFunction>
) => void;

/**
* Tells the SDK to use this function as the migration function of the contract.
* The migration function will ignore te existing state.
* @param _empty - An empty object.
*/
export function migrate(_empty: EmptyParameterObject): DecoratorFunction {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function <AnyFunction extends (...args: any) => any>(
_target: object,
_key: string | symbol,
_descriptor: TypedPropertyDescriptor<AnyFunction>
// eslint-disable-next-line @typescript-eslint/no-empty-function
): void {};
}

/**
* Tells the SDK to use this function as the initialization function of the contract.
*
Expand Down
39 changes: 39 additions & 0 deletions tests/__tests__/decorators/migrate.ava.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Worker } from "near-workspaces";
import test from "ava";

test.beforeEach(async (t) => {
const worker = await Worker.init();
const root = worker.rootAccount;
const counter = await root.devDeploy("./build/migrate.wasm");

const ali = await root.createSubAccount("ali");

t.context.worker = worker;
t.context.accounts = { root, counter, ali };
});

// If the environment is reused, use test.after to replace test.afterEach
test.afterEach.always(async (t) => {
await t.context.worker.tearDown().catch((error) => {
console.log("Failed to tear down the worker:", error);
});
});

test("migration works", async (t) => {
const { counter, ali } = t.context.accounts;

const res1 = await counter.view("getCount", {});
t.is(res1, 0);

await ali.call(counter, "increase", {});

const res2 = await counter.view("getCount", {});
t.is(res2, 1);

const migrationRes = await ali.callRaw(counter, "migrFuncValueTo18", {});

t.is(JSON.stringify(migrationRes).includes("Count: 0"), true);

const res3 = await counter.view("getCount", {});
t.is(res3, 18);
});
2 changes: 2 additions & 0 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"build:bigint-serialization": "near-sdk-js build src/bigint-serialization.ts build/bigint-serialization.wasm",
"build:date-serialization": "near-sdk-js build src/date-serialization.ts build/date-serialization.wasm",
"build:middlewares": "near-sdk-js build src/middlewares.ts build/middlewares.wasm",
"build:migrate": "near-sdk-js build src/migrate.ts build/migrate.wasm",
"test": "ava",
"test:context-api": "ava __tests__/test_context_api.ava.js",
"test:math-api": "ava __tests__/test_math_api.ava.js",
Expand All @@ -51,6 +52,7 @@
"test:date-serialization": "ava __tests__/test-date-serialization.ava.js",
"test:serialization": "ava __tests__/test-serialization.ava.js",
"test:constructor-validation": "ava __tests__/constructor_validation.ava.js",
"test:migrate": "ava __tests__/decorators/migrate.ava.js",
"test:middlewares": "ava __tests__/test-middlewares.ava.js"
},
"author": "Near Inc <hello@nearprotocol.com>",
Expand Down
23 changes: 23 additions & 0 deletions tests/src/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NearBindgen, near, call, view, migrate } from "near-sdk-js";

@NearBindgen({})
export class Counter {
count = 0;

@call({})
increase({ n = 1 }: { n: number }) {
this.count += n;
near.log(`Counter increased to ${this.count}`);
}

@view({})
getCount(): number {
return this.count;
}

@migrate({})
migrFuncValueTo18(): void {
near.log("Count: " + this.count); // expected to be 0
this.count = 18;
}
}

0 comments on commit 755e2fd

Please sign in to comment.