Skip to content

Commit

Permalink
Add asa metadata hash example and fix metadata validation (#486)
Browse files Browse the repository at this point in the history
* fix: metadataHash attribute verification for ASADefSchema

+ Added metadataHash usage examples in the asa template.

* test update

* add more tests

* update asa example
  • Loading branch information
robert-zaremba authored Oct 18, 2021
1 parent 291bad6 commit 720826b
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 46 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Bug Fixes
+ [web] Fixed `metadataHash` attribute verification for `ASADefSchema` and consequently `deployASA`.

## v2.0.0 2021-09-30

### Improvements
Expand Down
3 changes: 2 additions & 1 deletion examples/asa/assets/asa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ gold:
defaultFrozen: false
unitName: "GLD"
url: "url"
# Uint8Array or UTF-8 string representation of a hash commitment with respect to the asset. Must be exactly 32 bytes long.
# User may get "signature validation failed" from node if shorter hash is used.
metadataHash: "12312442142141241244444411111133"
note: "note"
Expand Down Expand Up @@ -57,4 +58,4 @@ alu:
manager: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
reserve: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
freeze: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
clawback: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
clawback: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
11 changes: 10 additions & 1 deletion examples/asa/scripts/0-gold-asa.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const crypto = require('crypto');

const { executeTransaction, balanceOf } = require('@algo-builder/algob');
const { mkParam } = require('./transfer/common');

/*
Create "gold" Algorand Standard Asset (ASA).
Accounts are loaded from config.
Expand All @@ -25,6 +26,13 @@ async function run (runtimeEnv, deployer) {
executeTransaction(deployer, mkParam(masterAccount, bob.addr, 1e6, { note: message }))];
await Promise.all(promises);

// create an assetMetadataHash as Uint8Array
const metadataHash = crypto.createHash('sha256').update('some content').digest();
// or UTF-8 string:
// let metadataHash = "this must be 32 chars long text."
// or from hex:
// let metadataHash = Buffer.from('664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex')

// Let's deploy ASA. The following commnad will open the `assets/asa.yaml` file and search for
// the `gold` ASA. The transaction can specify standard transaction parameters. If skipped
// node suggested values will be used.
Expand All @@ -35,6 +43,7 @@ async function run (runtimeEnv, deployer) {
// firstValid: 10,
// validRounds: 1002
}, {
metadataHash,
reserve: bob.addr // override default value set in asa.yaml
// freeze: bob.addr
// note: "gold-asa"
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime/src/lib/asa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ export function validateOptInAccNames (accounts: AccountMap | RuntimeAccountMap,
}

/**
* Validate and parse each field of asset definition
* Validate and parse each field of asset definition. `metadataHash`, if provided as a Buffer
* will be transformed into Uint8Array.
* @param asaDef asset definition
* @param source source of assetDef: asa.yaml file OR function deployASA
* @returns parsed asa definition
*/
export function parseASADef (asaDef: types.ASADef, source?: string): types.ASADef {
try {
if (asaDef.metadataHash && asaDef.metadataHash instanceof Buffer) {
asaDef.metadataHash = new Uint8Array(asaDef.metadataHash);
}
const parsedDef = ASADefSchema.parse(asaDef);
parsedDef.manager = parsedDef.manager !== "" ? parsedDef.manager : undefined;
parsedDef.reserve = parsedDef.reserve !== "" ? parsedDef.reserve : undefined;
Expand Down
64 changes: 55 additions & 9 deletions packages/runtime/test/src/lib/asa.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { types } from "@algo-builder/web";
import { assert } from "chai";
import * as crypto from "crypto";

import { RUNTIME_ERRORS } from "../../../src/errors/errors-list";
import { validateASADefs } from "../../../src/lib/asa";
Expand Down Expand Up @@ -50,7 +51,7 @@ describe("ASA parser", () => {
defaultFrozen: true,
unitName: "unitName",
url: "url",
metadataHash: "metadataHash",
metadataHash: "32ByteLongString32ByteLongString",
note: "note",
noteb64: "noteb64",
manager: "manager",
Expand All @@ -67,7 +68,7 @@ describe("ASA parser", () => {
defaultFrozen: true,
freeze: "freeze",
manager: "manager",
metadataHash: "metadataHash",
metadataHash: "32ByteLongString32ByteLongString",
note: "note",
noteb64: "noteb64",
reserve: "reserve",
Expand Down Expand Up @@ -188,22 +189,67 @@ describe("ASA parser", () => {
);
});

it("Should validate metadataHash; too long", async () => {
const obj = {
it("Should validate metadataHash", async () => {
/** negative paths **/

// check utf-8 strings
const asaDefs = {
A1: {
total: 1,
decimals: 1,
unitName: 'ASA',
// more than 32 bytes:
metadataHash: "1234567890abcdef1234567890abcdef_",
metadataHash: "1234567890abcdef1234567890xyzklmnone_",
defaultFrozen: false
}
} as types.ASADef
};
expectRuntimeError(
() => validateASADefs(obj, new Map<string, Account>(), ""),

const expectFail = (msg: string): void => expectRuntimeError(
() => validateASADefs(asaDefs, new Map<string, Account>(), ""),
RUNTIME_ERRORS.ASA.PARAM_PARSE_ERROR,
"metadataHash"
"Metadata Hash must be a 32 byte",
msg
);

expectFail("metadataHash too long");

asaDefs.A1.metadataHash = "too short";
expectFail("metadataHash too short");

// check with bytes array

asaDefs.A1.metadataHash = new Uint8Array(
Buffer.from('aaabbba664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex'));
expectFail("byte array too long");

asaDefs.A1.metadataHash = new Uint8Array(Buffer.from('a8', 'hex'));
expectFail("byte array too short");

// digest with a parameter will return string ... which will be too long
const content = "some content";
asaDefs.A1.metadataHash = crypto.createHash('sha256').update(content).digest("hex");
expectFail("Hex string should be converted to a UInt8Array");

/** potitive test **/

asaDefs.A1.metadataHash = new Uint8Array(
Buffer.from('664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex'));
let a = validateASADefs(asaDefs, new Map<string, Account>(), "");
assert.instanceOf(a.A1.metadataHash, Uint8Array, "valid, 64-long character hex string converted to byte array must work");

// digest with no parameters returns Buffer
asaDefs.A1.metadataHash = crypto.createHash('sha256').update(content).digest();
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
assert.instanceOf(a.A1.metadataHash, Uint8Array, "sha256 hash buffer must work");

// digest with no parameters returns Buffer
asaDefs.A1.metadataHash = new Uint8Array(crypto.createHash('sha256').update(content).digest());
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
assert.instanceOf(a.A1.metadataHash, Uint8Array, "sha256 hash bytes array must work");

asaDefs.A1.metadataHash = "s".repeat(32);
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
assert.typeOf(a.A1.metadataHash, 'string', "valid, 64-long character hex string converted to byte array must work");
});

it("Should check existence of opt-in account name accounts; green path", async () => {
Expand Down
54 changes: 20 additions & 34 deletions packages/web/src/types-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,36 @@ import * as z from 'zod';
export const AddressSchema = z.string();

// https://developer.algorand.org/docs/reference/rest-apis/algod/
const metadataRegex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==\|[A-Za-z0-9+/]{3}=)?$/;
const totalRegex = /^\d+$/;

export const ASADefSchema = z.object({
total: z.union([z.number(), z.bigint(), z.string()]), // 'string' to support bigint from yaml file
decimals: z.union([z.number(), z.bigint()]),
total: z.union([z.number(), z.bigint(), z.string()]) // 'string' to support bigint from yaml file
.refine(
t => (totalRegex.test(String(t)) && BigInt(t) <= 0xFFFFFFFFFFFFFFFFn && BigInt(t) > 0n),
{ message: "Total must be a positive number and smaller than 2^64-1 " }),
decimals: z.union([z.number(), z.bigint()]).refine(
decimals => ((decimals <= 19) && (decimals >= 0)),
{ message: "Decimals must be between 0 (non divisible) and 19" }),
defaultFrozen: z.boolean().optional(),
unitName: z.string().optional(),
url: z.string().optional(),
metadataHash: z.any().optional(),
unitName: z.string().optional().refine(
unitName => (!unitName || unitName.length <= 8),
{ message: "Unit name must not be longer than 8 bytes" }),
url: z.string().optional()
.refine(
url => (!url || url.length <= 96),
{ message: "URL must not be longer than 96 bytes" }),
metadataHash: z.string().or(z.instanceof(Buffer)).or(z.instanceof(Uint8Array)).optional().refine(
m => (!m ||
(typeof m === "string" && Buffer.from(m).byteLength === 32) ||
(m instanceof Uint8Array && m.length === 32)),
{ message: "Metadata Hash must be a 32 byte Uint8Array or 32 byte string" }),
note: z.string().optional(),
noteb64: z.string().optional(),
manager: AddressSchema.optional(),
reserve: AddressSchema.optional(),
freeze: AddressSchema.optional(),
clawback: AddressSchema.optional(),
optInAccNames: z.array(z.string()).optional()
})
.refine(o => (totalRegex.test(String(o.total)) &&
BigInt(o.total) <= 0xFFFFFFFFFFFFFFFFn && BigInt(o.total) > 0n), {
message: "Total must be a positive number and smaller than 2^64-1 ",
path: ['total']
})
.refine(o => ((o.decimals <= 19) && (o.decimals >= 0)), {
message: "Decimals must be between 0(non divisible) and 19",
path: ['decimals']
})
.refine(o => (!o.unitName || (o.unitName && (o.unitName.length <= 8))), {
message: "Unit name must not be longer than 8 bytes",
path: ['unitName']
})
// https://developer.algorand.org/articles/introducing-algorand-virtual-machine-avm-09-release/
.refine(o => (!o.url || (o.url && (o.url.length <= 96))), {
message: "URL must not be longer than 96 bytes",
path: ['url']
})
.refine(o => (!o.metadataHash || (o.metadataHash && (o.metadataHash.length <= 32))), {
message: "Metadata Hash must not be longer than 32 bytes",
path: ['metadataHash']
})
.refine(o => (!o.metadataHash || (o.metadataHash && (metadataRegex.test(o.metadataHash)))), {
message: "metadataHash doesn't match regex from " +
"https://developer.algorand.org/docs/reference/rest-apis/algod/",
path: ['metadataHash']
});
});

export const ASADefsSchema = z.record(ASADefSchema);

0 comments on commit 720826b

Please sign in to comment.