Khanetor
Khanetor

Reputation: 12302

How should I handle smart contract structure change in NEAR protocol?

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

Answers (1)

tifrel
tifrel

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

Related Questions