Skip to content

Commit 720826b

Browse files
Add asa metadata hash example and fix metadata validation (#486)
* fix: metadataHash attribute verification for ASADefSchema + Added metadataHash usage examples in the asa template. * test update * add more tests * update asa example
1 parent 291bad6 commit 720826b

File tree

6 files changed

+95
-46
lines changed

6 files changed

+95
-46
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
### Bug Fixes
6+
+ [web] Fixed `metadataHash` attribute verification for `ASADefSchema` and consequently `deployASA`.
7+
58
## v2.0.0 2021-09-30
69

710
### Improvements

examples/asa/assets/asa.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ gold:
44
defaultFrozen: false
55
unitName: "GLD"
66
url: "url"
7+
# Uint8Array or UTF-8 string representation of a hash commitment with respect to the asset. Must be exactly 32 bytes long.
78
# User may get "signature validation failed" from node if shorter hash is used.
89
metadataHash: "12312442142141241244444411111133"
910
note: "note"
@@ -57,4 +58,4 @@ alu:
5758
manager: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
5859
reserve: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
5960
freeze: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
60-
clawback: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"
61+
clawback: "WWYNX3TKQYVEREVSW6QQP3SXSFOCE3SKUSEIVJ7YAGUPEACNI5UGI4DZCE"

examples/asa/scripts/0-gold-asa.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
const crypto = require('crypto');
2+
13
const { executeTransaction, balanceOf } = require('@algo-builder/algob');
24
const { mkParam } = require('./transfer/common');
3-
45
/*
56
Create "gold" Algorand Standard Asset (ASA).
67
Accounts are loaded from config.
@@ -25,6 +26,13 @@ async function run (runtimeEnv, deployer) {
2526
executeTransaction(deployer, mkParam(masterAccount, bob.addr, 1e6, { note: message }))];
2627
await Promise.all(promises);
2728

29+
// create an assetMetadataHash as Uint8Array
30+
const metadataHash = crypto.createHash('sha256').update('some content').digest();
31+
// or UTF-8 string:
32+
// let metadataHash = "this must be 32 chars long text."
33+
// or from hex:
34+
// let metadataHash = Buffer.from('664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex')
35+
2836
// Let's deploy ASA. The following commnad will open the `assets/asa.yaml` file and search for
2937
// the `gold` ASA. The transaction can specify standard transaction parameters. If skipped
3038
// node suggested values will be used.
@@ -35,6 +43,7 @@ async function run (runtimeEnv, deployer) {
3543
// firstValid: 10,
3644
// validRounds: 1002
3745
}, {
46+
metadataHash,
3847
reserve: bob.addr // override default value set in asa.yaml
3948
// freeze: bob.addr
4049
// note: "gold-asa"

packages/runtime/src/lib/asa.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ export function validateOptInAccNames (accounts: AccountMap | RuntimeAccountMap,
3636
}
3737

3838
/**
39-
* Validate and parse each field of asset definition
39+
* Validate and parse each field of asset definition. `metadataHash`, if provided as a Buffer
40+
* will be transformed into Uint8Array.
4041
* @param asaDef asset definition
4142
* @param source source of assetDef: asa.yaml file OR function deployASA
4243
* @returns parsed asa definition
4344
*/
4445
export function parseASADef (asaDef: types.ASADef, source?: string): types.ASADef {
4546
try {
47+
if (asaDef.metadataHash && asaDef.metadataHash instanceof Buffer) {
48+
asaDef.metadataHash = new Uint8Array(asaDef.metadataHash);
49+
}
4650
const parsedDef = ASADefSchema.parse(asaDef);
4751
parsedDef.manager = parsedDef.manager !== "" ? parsedDef.manager : undefined;
4852
parsedDef.reserve = parsedDef.reserve !== "" ? parsedDef.reserve : undefined;

packages/runtime/test/src/lib/asa.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { types } from "@algo-builder/web";
22
import { assert } from "chai";
3+
import * as crypto from "crypto";
34

45
import { RUNTIME_ERRORS } from "../../../src/errors/errors-list";
56
import { validateASADefs } from "../../../src/lib/asa";
@@ -50,7 +51,7 @@ describe("ASA parser", () => {
5051
defaultFrozen: true,
5152
unitName: "unitName",
5253
url: "url",
53-
metadataHash: "metadataHash",
54+
metadataHash: "32ByteLongString32ByteLongString",
5455
note: "note",
5556
noteb64: "noteb64",
5657
manager: "manager",
@@ -67,7 +68,7 @@ describe("ASA parser", () => {
6768
defaultFrozen: true,
6869
freeze: "freeze",
6970
manager: "manager",
70-
metadataHash: "metadataHash",
71+
metadataHash: "32ByteLongString32ByteLongString",
7172
note: "note",
7273
noteb64: "noteb64",
7374
reserve: "reserve",
@@ -188,22 +189,67 @@ describe("ASA parser", () => {
188189
);
189190
});
190191

191-
it("Should validate metadataHash; too long", async () => {
192-
const obj = {
192+
it("Should validate metadataHash", async () => {
193+
/** negative paths **/
194+
195+
// check utf-8 strings
196+
const asaDefs = {
193197
A1: {
194198
total: 1,
195199
decimals: 1,
196200
unitName: 'ASA',
197201
// more than 32 bytes:
198-
metadataHash: "1234567890abcdef1234567890abcdef_",
202+
metadataHash: "1234567890abcdef1234567890xyzklmnone_",
199203
defaultFrozen: false
200-
}
204+
} as types.ASADef
201205
};
202-
expectRuntimeError(
203-
() => validateASADefs(obj, new Map<string, Account>(), ""),
206+
207+
const expectFail = (msg: string): void => expectRuntimeError(
208+
() => validateASADefs(asaDefs, new Map<string, Account>(), ""),
204209
RUNTIME_ERRORS.ASA.PARAM_PARSE_ERROR,
205-
"metadataHash"
210+
"Metadata Hash must be a 32 byte",
211+
msg
206212
);
213+
214+
expectFail("metadataHash too long");
215+
216+
asaDefs.A1.metadataHash = "too short";
217+
expectFail("metadataHash too short");
218+
219+
// check with bytes array
220+
221+
asaDefs.A1.metadataHash = new Uint8Array(
222+
Buffer.from('aaabbba664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex'));
223+
expectFail("byte array too long");
224+
225+
asaDefs.A1.metadataHash = new Uint8Array(Buffer.from('a8', 'hex'));
226+
expectFail("byte array too short");
227+
228+
// digest with a parameter will return string ... which will be too long
229+
const content = "some content";
230+
asaDefs.A1.metadataHash = crypto.createHash('sha256').update(content).digest("hex");
231+
expectFail("Hex string should be converted to a UInt8Array");
232+
233+
/** potitive test **/
234+
235+
asaDefs.A1.metadataHash = new Uint8Array(
236+
Buffer.from('664143504f346e52674f35356a316e64414b3357365367633441506b63794668', 'hex'));
237+
let a = validateASADefs(asaDefs, new Map<string, Account>(), "");
238+
assert.instanceOf(a.A1.metadataHash, Uint8Array, "valid, 64-long character hex string converted to byte array must work");
239+
240+
// digest with no parameters returns Buffer
241+
asaDefs.A1.metadataHash = crypto.createHash('sha256').update(content).digest();
242+
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
243+
assert.instanceOf(a.A1.metadataHash, Uint8Array, "sha256 hash buffer must work");
244+
245+
// digest with no parameters returns Buffer
246+
asaDefs.A1.metadataHash = new Uint8Array(crypto.createHash('sha256').update(content).digest());
247+
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
248+
assert.instanceOf(a.A1.metadataHash, Uint8Array, "sha256 hash bytes array must work");
249+
250+
asaDefs.A1.metadataHash = "s".repeat(32);
251+
a = validateASADefs(asaDefs, new Map<string, Account>(), "");
252+
assert.typeOf(a.A1.metadataHash, 'string', "valid, 64-long character hex string converted to byte array must work");
207253
});
208254

209255
it("Should check existence of opt-in account name accounts; green path", async () => {

packages/web/src/types-input.ts

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,36 @@ import * as z from 'zod';
33
export const AddressSchema = z.string();
44

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

98
export const ASADefSchema = z.object({
10-
total: z.union([z.number(), z.bigint(), z.string()]), // 'string' to support bigint from yaml file
11-
decimals: z.union([z.number(), z.bigint()]),
9+
total: z.union([z.number(), z.bigint(), z.string()]) // 'string' to support bigint from yaml file
10+
.refine(
11+
t => (totalRegex.test(String(t)) && BigInt(t) <= 0xFFFFFFFFFFFFFFFFn && BigInt(t) > 0n),
12+
{ message: "Total must be a positive number and smaller than 2^64-1 " }),
13+
decimals: z.union([z.number(), z.bigint()]).refine(
14+
decimals => ((decimals <= 19) && (decimals >= 0)),
15+
{ message: "Decimals must be between 0 (non divisible) and 19" }),
1216
defaultFrozen: z.boolean().optional(),
13-
unitName: z.string().optional(),
14-
url: z.string().optional(),
15-
metadataHash: z.any().optional(),
17+
unitName: z.string().optional().refine(
18+
unitName => (!unitName || unitName.length <= 8),
19+
{ message: "Unit name must not be longer than 8 bytes" }),
20+
url: z.string().optional()
21+
.refine(
22+
url => (!url || url.length <= 96),
23+
{ message: "URL must not be longer than 96 bytes" }),
24+
metadataHash: z.string().or(z.instanceof(Buffer)).or(z.instanceof(Uint8Array)).optional().refine(
25+
m => (!m ||
26+
(typeof m === "string" && Buffer.from(m).byteLength === 32) ||
27+
(m instanceof Uint8Array && m.length === 32)),
28+
{ message: "Metadata Hash must be a 32 byte Uint8Array or 32 byte string" }),
1629
note: z.string().optional(),
1730
noteb64: z.string().optional(),
1831
manager: AddressSchema.optional(),
1932
reserve: AddressSchema.optional(),
2033
freeze: AddressSchema.optional(),
2134
clawback: AddressSchema.optional(),
2235
optInAccNames: z.array(z.string()).optional()
23-
})
24-
.refine(o => (totalRegex.test(String(o.total)) &&
25-
BigInt(o.total) <= 0xFFFFFFFFFFFFFFFFn && BigInt(o.total) > 0n), {
26-
message: "Total must be a positive number and smaller than 2^64-1 ",
27-
path: ['total']
28-
})
29-
.refine(o => ((o.decimals <= 19) && (o.decimals >= 0)), {
30-
message: "Decimals must be between 0(non divisible) and 19",
31-
path: ['decimals']
32-
})
33-
.refine(o => (!o.unitName || (o.unitName && (o.unitName.length <= 8))), {
34-
message: "Unit name must not be longer than 8 bytes",
35-
path: ['unitName']
36-
})
37-
// https://developer.algorand.org/articles/introducing-algorand-virtual-machine-avm-09-release/
38-
.refine(o => (!o.url || (o.url && (o.url.length <= 96))), {
39-
message: "URL must not be longer than 96 bytes",
40-
path: ['url']
41-
})
42-
.refine(o => (!o.metadataHash || (o.metadataHash && (o.metadataHash.length <= 32))), {
43-
message: "Metadata Hash must not be longer than 32 bytes",
44-
path: ['metadataHash']
45-
})
46-
.refine(o => (!o.metadataHash || (o.metadataHash && (metadataRegex.test(o.metadataHash)))), {
47-
message: "metadataHash doesn't match regex from " +
48-
"https://developer.algorand.org/docs/reference/rest-apis/algod/",
49-
path: ['metadataHash']
50-
});
36+
});
5137

5238
export const ASADefsSchema = z.record(ASADefSchema);

0 commit comments

Comments
 (0)