Skip to content

Commit

Permalink
Using static variable to configure field numbering strategy (#2939)
Browse files Browse the repository at this point in the history
* feat: option for the user to choose between legacy and updated field numbering

* chore: fix typo

* chore: add typedoc comments for Token.fieldNumberingStrategy

* test: fix failing integration test
  • Loading branch information
ac10n committed May 2, 2024
1 parent 9f27a72 commit 170f61f
Show file tree
Hide file tree
Showing 10 changed files with 1,179 additions and 168 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FieldNumberingStrategy } from "@taquito/michelson-encoder";
import { CONFIGS } from "../../config";
import { noAnnotCode, noAnnotInit } from "../../data/token_without_annotation";

Expand All @@ -10,59 +11,67 @@ CONFIGS().forEach(({ lib, rpc, setup }) => {
beforeEach(async () => {
await setup()
})
it('Verify contract.originate for a contract with no annotations for methods using methodObjects', async () => {
// Constants to replace annotations
const ACCOUNTS = '0';
const BALANCE = '0';
const ALLOWANCES = '1';
const TRANSFER = '0';
const APPROVE = '2';

// Actual tests
// Constants to replace annotations
const ACCOUNTS = '0';
const BALANCE = '0';
const ALLOWANCES = '1';
const TRANSFER = '0';
const APPROVE = '2';

const ACCOUNT1_ADDRESS = await Tezos.signer.publicKeyHash()
const ACCOUNT2_ADDRESS = 'tz1ZfrERcALBwmAqwonRXYVQBDT9BjNjBHJu'
// Actual tests

// Originate a contract with a known state
const op = await Tezos.contract.originate({
balance: "1",
code: noAnnotCode,
init: noAnnotInit(await Tezos.signer.publicKeyHash())
})
await op.confirmation()
const contract = await op.contract()
const ACCOUNT2_ADDRESS = 'tz1ZfrERcALBwmAqwonRXYVQBDT9BjNjBHJu'

const testContract = (strategy: FieldNumberingStrategy, innerObjectStartingIndex: number) => {
it(`Verify contract.originate for a contract with no annotations for methods using methodObjects with fieldNumberingStrategy: ${strategy}`, async () => {
Tezos.setFieldNumberingStrategy(strategy);
const ACCOUNT1_ADDRESS = await Tezos.signer.publicKeyHash()
// Originate a contract with a known state
const op = await Tezos.contract.originate({
balance: "1",
code: noAnnotCode,
init: noAnnotInit(await Tezos.signer.publicKeyHash())
})
await op.confirmation()
const contract = await op.contract()

// Make a transfer
// Make a transfer

const operation = await contract.methodsObject[TRANSFER]({
0: ACCOUNT1_ADDRESS,
1: ACCOUNT2_ADDRESS,
2: "1"
}).send();
const operation = await contract.methodsObject[TRANSFER]({
0: ACCOUNT1_ADDRESS,
1: ACCOUNT2_ADDRESS,
2: "1"
}).send();

await operation.confirmation();
expect(operation.status).toEqual('applied')
await operation.confirmation();
expect(operation.status).toEqual('applied')

// Verify that the transfer was done as expected
const storage = await contract.storage<any>()
let account1 = await storage[ACCOUNTS].get(ACCOUNT1_ADDRESS)
expect(account1[BALANCE].toString()).toEqual('16')
// Verify that the transfer was done as expected
const storage = await contract.storage<any>()
let account1 = await storage[ACCOUNTS].get(ACCOUNT1_ADDRESS)
expect(account1[BALANCE].toString()).toEqual('16')

const account2 = await storage[ACCOUNTS].get(ACCOUNT2_ADDRESS)
expect(account2[BALANCE].toString()).toEqual('1')
const account2 = await storage[ACCOUNTS].get(ACCOUNT2_ADDRESS)
expect(account2[BALANCE].toString()).toEqual('1')

// Approve
const operation2 = await contract.methodsObject[APPROVE]({
0: ACCOUNT2_ADDRESS,
1: "1"
}).send();
// Approve
const operation2 = await contract.methodsObject[APPROVE]({
[innerObjectStartingIndex]: ACCOUNT2_ADDRESS,
[innerObjectStartingIndex + 1]: "1"
}).send();

await operation2.confirmation();
expect(operation2.status).toEqual('applied')
await operation2.confirmation();
expect(operation2.status).toEqual('applied')

// Verify that the allowance was done as expected
account1 = await storage[ACCOUNTS].get(ACCOUNT1_ADDRESS)
expect(account1[ALLOWANCES].get(ACCOUNT2_ADDRESS).toString()).toEqual('1')
});
};
testContract('Legacy', 2);
testContract('ResetFieldNumbersInNestedObjects', 0);
testContract('Latest', 0);

// Verify that the allowance was done as expected
account1 = await storage[ACCOUNTS].get(ACCOUNT1_ADDRESS)
expect(account1[ALLOWANCES].get(ACCOUNT2_ADDRESS).toString()).toEqual('1')
})
});
})
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export const UnitValue = Symbol();
export const SaplingStateValue = {};
export * from './michelson-map';
export { VERSION } from './version';
export { Token } from './tokens/token';
export { FieldNumberingStrategy, Token } from './tokens/token';
53 changes: 40 additions & 13 deletions packages/taquito-michelson-encoder/src/tokens/or.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ export class OrToken extends ComparableToken {
public Encode(args: any[]): any {
const label = args[args.length - 1];

const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}

const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);

if (String(leftToken.annot()) === String(label) && !(leftToken instanceof OrToken)) {
args.pop();
Expand All @@ -71,13 +75,17 @@ export class OrToken extends ComparableToken {
}

public ExtractSignature(): any {
const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}

const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);

const newSig = [];

Expand Down Expand Up @@ -107,13 +115,17 @@ export class OrToken extends ComparableToken {
this.validateJavascriptObject(args);
const label = Object.keys(args)[0];

const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}

const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);

if (String(leftToken.annot()) === String(label) && !(leftToken instanceof OrToken)) {
return { prim: 'Left', args: [leftToken.EncodeObject(args[label], semantic)] };
Expand Down Expand Up @@ -159,12 +171,16 @@ export class OrToken extends ComparableToken {
* @throws {@link OrValidationError}
*/
public Execute(val: any, semantics?: Semantic): any {
const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}
const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);

if (val.prim === 'Right') {
if (rightToken instanceof OrToken) {
Expand Down Expand Up @@ -195,7 +211,7 @@ export class OrToken extends ComparableToken {
getRightValue: (token: Token) => any,
concat: (left: any, right: any) => any
) {
const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
let leftValue;
if (leftToken instanceof OrToken) {
Expand All @@ -205,7 +221,11 @@ export class OrToken extends ComparableToken {
leftValue = { [leftToken.annot()]: getLeftValue(leftToken) };
}

const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);
let rightValue;
if (rightToken instanceof OrToken) {
rightValue = getRightValue(rightToken);
Expand Down Expand Up @@ -260,13 +280,17 @@ export class OrToken extends ComparableToken {
}

private findToken(label: any): Token | null {
const leftToken = this.createToken(this.val.args[0], this.getIdx(), 'Or');
const leftToken = this.createToken(this.val.args[0], this.getIdxForChildren(), 'Or');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}

const rightToken = this.createToken(this.val.args[1], this.getIdx() + keyCount, 'Or');
const rightToken = this.createToken(
this.val.args[1],
this.getIdxForChildren() + keyCount,
'Or'
);

if (
String(leftToken.annot()) === String(label) &&
Expand Down Expand Up @@ -340,7 +364,10 @@ export class OrToken extends ComparableToken {
return tokens;
}

protected getIdx(): number {
protected getIdxForChildren(): number {
if (Token.fieldNumberingStrategy === 'Legacy') {
return this.idx;
}
return this.parentTokenType === 'Or' ? this.idx : 0;
}
}
15 changes: 9 additions & 6 deletions packages/taquito-michelson-encoder/src/tokens/pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class PairToken extends ComparableToken {
private tokens(): [Token, Token] {
let cnt = 0;
return this.args().map((a) => {
const tok = this.createToken(a, this.getIdx() + cnt, 'Pair');
const tok = this.createToken(a, this.getIdxForChildren() + cnt, 'Pair');
if (tok instanceof PairToken) {
cnt += Object.keys(tok.ExtractSchema()).length;
} else {
Expand All @@ -116,13 +116,13 @@ export class PairToken extends ComparableToken {

public ExtractSignature(): any {
const args = this.args();
const leftToken = this.createToken(args[0], this.getIdx(), 'Pair');
const leftToken = this.createToken(args[0], this.getIdxForChildren(), 'Pair');
let keyCount = 1;
if (leftToken instanceof OrToken) {
keyCount = Object.keys(leftToken.ExtractSchema()).length;
}

const rightToken = this.createToken(args[1], this.getIdx() + keyCount, 'Pair');
const rightToken = this.createToken(args[1], this.getIdxForChildren() + keyCount, 'Pair');

const newSig = [];

Expand Down Expand Up @@ -175,7 +175,7 @@ export class PairToken extends ComparableToken {
private traversal(getLeftValue: (token: Token) => any, getRightValue: (token: Token) => any) {
const args = this.args();

const leftToken = this.createToken(args[0], this.getIdx(), 'Pair');
const leftToken = this.createToken(args[0], this.getIdxForChildren(), 'Pair');
let keyCount = 1;
let leftValue;
if (leftToken instanceof PairToken && !leftToken.hasAnnotations()) {
Expand All @@ -187,7 +187,7 @@ export class PairToken extends ComparableToken {
leftValue = { [leftToken.annot()]: getLeftValue(leftToken) };
}

const rightToken = this.createToken(args[1], this.getIdx() + keyCount, 'Pair');
const rightToken = this.createToken(args[1], this.getIdxForChildren() + keyCount, 'Pair');
let rightValue;
if (rightToken instanceof PairToken && !rightToken.hasAnnotations()) {
rightValue = getRightValue(rightToken);
Expand Down Expand Up @@ -282,7 +282,10 @@ export class PairToken extends ComparableToken {
return tokens;
}

protected getIdx(): number {
protected getIdxForChildren(): number {
if (Token.fieldNumberingStrategy === 'Legacy') {
return this.idx;
}
return this.parentTokenType === 'Pair' ? this.idx : 0;
}
}
24 changes: 24 additions & 0 deletions packages/taquito-michelson-encoder/src/tokens/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,31 @@ export interface SemanticEncoding {
[key: string]: (value: any, type?: MichelsonV1Expression) => MichelsonV1Expression;
}

/**
* @description Possible strategies for mapping between javascript classes and Michelson values
* Legacy: The old behaviour: { annot1: 'some value', annot2: 'other Value', annot3: { 2: 'yet another value', 3: 'also some value' }}
* ResetFieldNumbersInNestedObjects: { annot1: 'some value', annot2: 'other Value', annot3: { 0: 'yet another value', 1: 'also some value' }}
* Latest: This will include new changes as we might implement in the future. This is the suggested value if it does not break your code
*/
export type FieldNumberingStrategy = 'Legacy' | 'ResetFieldNumbersInNestedObjects' | 'Latest';

export abstract class Token {
private static _fieldNumberingStrategy: FieldNumberingStrategy = 'Latest';

/**
* @description Gets the strategy used for field numbering in Token execute/encode/decode to convert Michelson values to/from javascript objects, returns a value of type {@link FieldNumberingStrategy} that controls how field numbers are calculated
*/
static get fieldNumberingStrategy() {
return Token._fieldNumberingStrategy;
}

/**
* @description Sets the strategy used for field numbering in Token execute/encode/decode to convert Michelson values to/from javascript objects, accepts a value of type {@link FieldNumberingStrategy} that controls how field numbers are calculated
*/
static set fieldNumberingStrategy(value: FieldNumberingStrategy) {
Token._fieldNumberingStrategy = value;
}

constructor(
protected val: MichelsonV1ExpressionExtended,
protected idx: number,
Expand Down
Loading

0 comments on commit 170f61f

Please sign in to comment.