Reputation: 12302
Suppose I have a contract
...
pub struct Contract {
collection_a: Vector<String>,
}
After I deploy this version, when I change the data structure of my smart contract, for e.g.
pub struct Contract {
collection_a: Vector<String>,
collection_b: Vector<String>,
}
I ran into an error when interacting with the contract
Failure [dev-1644158197214-15380220543819]: Error: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: \"Unexpected length of input\" }', /workspace/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46"}}
ServerTransactionError: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: \"Unexpected length of input\" }', /workspace/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46"}}
at Object.parseResultError (/home/gitpod/.nvm/versions/node/v16.13.0/lib/node_modules/near-cli/node_modules/near-api-js/lib/utils/rpc_errors.js:31:29)
at Account.signAndSendTransactionV2 (/home/gitpod/.nvm/versions/node/v16.13.0/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:160:36)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async scheduleFunctionCall (/home/gitpod/.nvm/versions/node/v16.13.0/lib/node_modules/near-cli/commands/call.js:57:38)
at async Object.handler (/home/gitpod/.nvm/versions/node/v16.13.0/lib/node_modules/near-cli/utils/exit-on-error.js:52:9) {
type: 'FunctionCallError',
context: undefined,
index: 0,
kind: {
ExecutionError: `Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: "Unexpected length of input" }', /workspace/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46`
},
transaction_outcome: {
proof: [ [Object], [Object] ],
block_hash: '5mPRmggsyL9cNsgS4a6mzRT7ua9Y8SS8XJbW9psawdDr',
id: '8BeARer3UXLoZ3Vr22QAqkyzsp143D7FCtVssjyYxzs',
outcome: {
logs: [],
receipt_ids: [Array],
gas_burnt: 2427936651538,
tokens_burnt: '242793665153800000000',
executor_id: 'dev-1644158197214-15380220543819',
status: [Object],
metadata: [Object]
}
}
}
How can I handle this situation when I need to update the structure?
Upvotes: 3
Views: 481
Reputation: 451
What you want is storage migration:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct OldContract {
collection_a: Vector<String>,
}
#[near_bindgen]
#[derive(BorshSerialize, BorshDeserialize)]
pub struct Contract {
collection_a: Vector<String>,
collection_b: Vector<String>,
}
#[near_bindgen]
impl Contract {
#[private]
#[init(ignore_state)]
pub fn migrate() -> Self {
let old_storage: OldContract = env::state_read().expect("Couldn't read old state");
Self {
collection_a: old_storage.collection_a,
collection_b: Vec::new(),
}
}
}
First you update your contract with this code, then you call the migrate
method with the contract key. On your next upgrade, you can delete the method and OldContract
struct to save on storage.
A problem you might run into are storage migrations that do not fit into a single block. AFAIK, there is no solution for that. Borsh serialization however is deterministic, so as long as you keep your data structures as enums, you should be able to just reinterpret the current chain storage and split migrations into multiple partial migrations. Make sure to thoroughly test that, you run the risk of irrecoverably screwing up your contracts state.
Upvotes: 1