diff --git a/src/etching.ts b/src/etching.ts index 1070aba..71b8f7b 100644 --- a/src/etching.ts +++ b/src/etching.ts @@ -1,16 +1,18 @@ import { None, Option, Some } from '@sniptt/monads'; import { Mint } from './mint'; import { Rune } from './rune'; +import { u128 } from './u128'; export class Etching { readonly symbol: Option; constructor( - readonly divisibility: number, + readonly divisibility: Option, readonly rune: Option, - readonly spacers: number, + readonly spacers: Option, symbol: Option, - readonly mint: Option + readonly mint: Option, + readonly premine: Option ) { this.symbol = symbol.andThen((value) => { const codePoint = value.codePointAt(0); diff --git a/src/mint.ts b/src/mint.ts index 60df86b..d2556ce 100644 --- a/src/mint.ts +++ b/src/mint.ts @@ -2,7 +2,8 @@ import { Option } from '@sniptt/monads'; import { u128 } from './u128'; export type Mint = { - deadline: Option; - limit: Option; - term: Option; + cap: Option; // mint cap + deadline: Option; // unix timestamp + limit: Option; // claim amount + term: Option; // relative block height }; diff --git a/src/rune.ts b/src/rune.ts index 1fb7d88..469b35c 100644 --- a/src/rune.ts +++ b/src/rune.ts @@ -1,6 +1,6 @@ import { Chain } from './chain'; import { RESERVED, SUBSIDY_HALVING_INTERVAL } from './constants'; -import { u128 } from './u128'; +import { U32_MAX, u128 } from './u128'; import _ from 'lodash'; export class Rune { @@ -60,7 +60,7 @@ export class Rune { let progress = u128.saturatingSub(offset, startSubsidyInterval); let length = u128.saturatingSub(u128(12n), u128(progress / INTERVAL)); - let lengthNumber = Number(length & 0xffff_ffffn); + let lengthNumber = Number(length & u128(U32_MAX)); let endStepInterval = Rune.STEPS[lengthNumber]; @@ -80,6 +80,19 @@ export class Rune { return this.value >= RESERVED; } + get commitment(): Buffer { + const bytes = Buffer.alloc(16); + bytes.writeBigUInt64LE(0xffffffff_ffffffffn & this.value, 0); + bytes.writeBigUInt64LE(this.value >> 64n, 8); + + let end = bytes.length; + while (end > 0 && bytes.at(end - 1) === 0) { + end--; + } + + return bytes.subarray(0, end); + } + static getReserved(n: u128): Rune { return new Rune(u128.checkedAdd(RESERVED, n).unwrap()); } diff --git a/src/runestone.ts b/src/runestone.ts index 868dd2d..fd56134 100644 --- a/src/runestone.ts +++ b/src/runestone.ts @@ -49,6 +49,10 @@ export class Runestone { } } + static cenotaph(): Runestone { + return new Runestone(true, None, None, [], None); + } + static decipher(transaction: bitcoin.Transaction): Option { const optionPayload = Runestone.payload(transaction); if (optionPayload.isNone()) { @@ -56,12 +60,12 @@ export class Runestone { } const payload = optionPayload.unwrap(); if (!isValidPayload(payload)) { - return Some(new Runestone(true, None, None, [], None)); + return Some(Runestone.cenotaph()); } const optionIntegers = Runestone.integers(payload); if (optionIntegers.isNone()) { - return Some(new Runestone(true, None, None, [], None)); + return Some(Runestone.cenotaph()); } const { cenotaph, edicts, fields } = Message.fromIntegers( @@ -105,16 +109,18 @@ export class Runestone { value <= 0xffn && Number(value) <= MAX_DIVISIBILITY ? Some(Number(value)) : None - ).unwrapOr(0); - - const limit = Tag.take(Tag.LIMIT, fields, 1, ([value]) => - value <= MAX_LIMIT ? Some(value) : None ); + const limit = Tag.take(Tag.LIMIT, fields, 1, ([value]) => Some(value)); + const rune = Tag.take(Tag.RUNE, fields, 1, ([value]) => Some(new Rune(value)) ); + const cap = Tag.take(Tag.CAP, fields, 1, ([value]) => Some(value)); + + const premine = Tag.take(Tag.PREMINE, fields, 1, ([value]) => Some(value)); + const spacers = Tag.take( Tag.SPACERS, fields, @@ -123,7 +129,7 @@ export class Runestone { value <= u128(U32_MAX) && Number(value) <= MAX_SPACERS ? Some(Number(value)) : None - ).unwrapOr(0); + ); const symbol = Tag.take(Tag.SYMBOL, fields, 1, ([value]) => { if (value > u128(U32_MAX)) { @@ -138,7 +144,7 @@ export class Runestone { }); const term = Tag.take(Tag.TERM, fields, 1, ([value]) => - value <= 0xffff_ffffn ? Some(Number(value)) : None + value <= U32_MAX ? Some(Number(value)) : None ); let flags = Tag.take(Tag.FLAGS, fields, 1, ([value]) => @@ -153,6 +159,18 @@ export class Runestone { const mint = mintResult.set; flags = mintResult.flags; + const overflow = (() => { + const premineU128 = premine.unwrapOr(u128(0)); + const capU128 = cap.unwrapOr(u128(0)); + const limitU128 = limit.unwrapOr(u128(0)); + + const multiplyResult = u128.checkedMultiply(capU128, limitU128); + if (multiplyResult.isNone()) { + return None; + } + return u128.checkedAdd(premineU128, multiplyResult.unwrap()); + })().isNone(); + let etching: Option = etch ? Some( new Etching( @@ -162,11 +180,13 @@ export class Runestone { symbol, mint ? Some({ + cap, deadline, limit, term, }) - : None + : None, + premine ) ) : None; @@ -174,6 +194,7 @@ export class Runestone { return Some( new Runestone( cenotaph || + overflow || flags !== 0n || [...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined, claim, @@ -203,14 +224,16 @@ export class Runestone { payloads.push(Tag.encode(Tag.RUNE, [rune.value])); } - if (etching.divisibility !== 0) { + if (etching.divisibility.isSome()) { payloads.push( - Tag.encode(Tag.DIVISIBILITY, [u128(etching.divisibility)]) + Tag.encode(Tag.DIVISIBILITY, [u128(etching.divisibility.unwrap())]) ); } - if (etching.spacers !== 0) { - payloads.push(Tag.encode(Tag.SPACERS, [u128(etching.spacers)])); + if (etching.spacers.isSome()) { + payloads.push( + Tag.encode(Tag.SPACERS, [u128(etching.spacers.unwrap())]) + ); } if (etching.symbol.isSome()) { @@ -218,6 +241,11 @@ export class Runestone { payloads.push(Tag.encode(Tag.SYMBOL, [u128(symbol.codePointAt(0)!)])); } + if (etching.premine.isSome()) { + const premine = etching.premine.unwrap(); + payloads.push(Tag.encode(Tag.SYMBOL, [premine])); + } + if (etching.mint.isSome()) { const mint = etching.mint.unwrap(); @@ -235,6 +263,11 @@ export class Runestone { const term = mint.term.unwrap(); payloads.push(Tag.encode(Tag.TERM, [u128(term)])); } + + if (mint.cap.isSome()) { + const cap = mint.cap.unwrap(); + payloads.push(Tag.encode(Tag.CAP, [cap])); + } } } diff --git a/src/tag.ts b/src/tag.ts index 9cecd14..f7edab3 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -12,6 +12,8 @@ export enum Tag { DEADLINE = 10, DEFAULT_OUTPUT = 12, CLAIM = 14, + CAP = 16, + PREMINE = 18, CENOTAPH = 126, DIVISIBILITY = 1, diff --git a/src/u128.ts b/src/u128.ts index 6614cfc..a70bc48 100644 --- a/src/u128.ts +++ b/src/u128.ts @@ -30,7 +30,7 @@ export type u128 = BigTypedNumber<'u128'>; export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; -export const U32_MAX = 0xffffffff; +export const U32_MAX = 0xffff_ffff; /** * Convert Number or BigInt to 128-bit unsigned integer. diff --git a/test/rune.test.ts b/test/rune.test.ts index 27a3014..70ee53b 100644 --- a/test/rune.test.ts +++ b/test/rune.test.ts @@ -181,4 +181,21 @@ describe('rune', () => { } } }); + + test('commitment', () => { + function testcase(rune: number | u128, bytes: number[]) { + expect([...new Rune(u128(rune)).commitment]).toEqual(bytes); + } + + testcase(0, []); + testcase(1, [1]); + testcase(255, [255]); + testcase(256, [0, 1]); + testcase(65535, [255, 255]); + testcase(65536, [0, 0, 1]); + testcase( + u128.MAX, + _.range(16).map(() => 255) + ); + }); }); diff --git a/test/runestone.test.ts b/test/runestone.test.ts index f142f12..b8bb7ca 100644 --- a/test/runestone.test.ts +++ b/test/runestone.test.ts @@ -189,9 +189,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.isNone()).toBe(true); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -208,9 +208,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -248,9 +248,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.isNone()).toBe(true); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); const mint = etching.mint.unwrap(); @@ -279,9 +279,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.isNone()).toBe(true); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); const mint = etching.mint.unwrap(); @@ -325,9 +325,9 @@ describe('runestone', () => { expect(runestone.cenotaph).toBe(true); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -352,9 +352,7 @@ describe('runestone', () => { expect(runestone.edicts).toEqual([ { id: createRuneId(1), amount: 2n, output: 0 }, ]); - expect(runestone.etching.unwrap()).toMatchObject({ - divisibility: 4, - }); + expect(runestone.etching.unwrap().divisibility.unwrap()).toBe(4); }); test('runestone_with_unrecognized_even_tag_is_cenotaph', () => { @@ -439,9 +437,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(5); + expect(etching.divisibility.unwrap()).toBe(5); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -468,9 +466,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -497,9 +495,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); expect(etching.mint.isNone()).toBe(true); }); @@ -526,9 +524,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(0); + expect(etching.divisibility.isNone()).toBe(true); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.unwrap()).toBe('a'); expect(etching.mint.isNone()).toBe(true); }); @@ -552,6 +550,16 @@ describe('runestone', () => { 2, Tag.LIMIT, 3, + Tag.PREMINE, + 8, + Tag.CAP, + 9, + Tag.DEFAULT_OUTPUT, + 0, + Tag.CLAIM, + 1, + Tag.CLAIM, + 1, Tag.BODY, 1, 1, @@ -563,17 +571,22 @@ describe('runestone', () => { expect(runestone.edicts).toEqual([ { id: createRuneId(1), amount: 2n, output: 0 }, ]); + expect(runestone.cenotaph).toBe(false); + expect(runestone.defaultOutput.unwrap()).toBe(0); + expect(runestone.claim.unwrap()).toEqual(RuneId.new(1, 1).unwrap()); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(1); + expect(etching.divisibility.unwrap()).toBe(1); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(5); + expect(etching.spacers.unwrap()).toBe(5); expect(etching.symbol.unwrap()).toBe('a'); + expect(etching.premine.unwrap()).toBe(8n); const mint = etching.mint.unwrap(); expect(mint.deadline.unwrap()).toBe(7); expect(mint.limit.unwrap()).toBe(3n); expect(mint.term.unwrap()).toBe(2); + expect(mint.cap.unwrap()).toBe(9n); }); test('recognized_even_etching_fields_in_non_etching_are_ignored', () => { @@ -629,9 +642,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(1); + expect(etching.divisibility.unwrap()).toBe(1); expect(etching.rune.unwrap().value).toBe(4n); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.unwrap()).toBe('a'); }); @@ -705,9 +718,9 @@ describe('runestone', () => { ]); const etching = runestone.etching.unwrap(); - expect(etching.divisibility).toBe(5); + expect(etching.divisibility.unwrap()).toBe(5); expect(etching.rune.isNone()).toBe(true); - expect(etching.spacers).toBe(0); + expect(etching.spacers.isNone()).toBe(true); expect(etching.symbol.isNone()).toBe(true); }); @@ -769,14 +782,21 @@ describe('runestone', () => { testcase( [], - Some(new Etching(0, Some(new Rune(u128(0))), 0, None, None)), + Some(new Etching(None, Some(new Rune(u128(0))), None, None, None, None)), 7 ); testcase( [], Some( - new Etching(MAX_DIVISIBILITY, Some(new Rune(u128(0))), 0, None, None) + new Etching( + Some(MAX_DIVISIBILITY), + Some(new Rune(u128(0))), + None, + None, + None, + None + ) ), 9 ); @@ -785,15 +805,17 @@ describe('runestone', () => { [], Some( new Etching( - MAX_DIVISIBILITY, + Some(MAX_DIVISIBILITY), Some(new Rune(u128(0))), - 1, + Some(1), Some('$'), Some({ deadline: Some(10000), limit: Some(u128(1)), term: Some(1), - }) + cap: None, + }), + None ) ), 20 @@ -801,7 +823,7 @@ describe('runestone', () => { testcase( [], - Some(new Etching(0, Some(new Rune(u128.MAX)), 0, None, None)), + Some(new Etching(None, Some(new Rune(u128.MAX)), None, None, None, None)), 25 ); @@ -814,7 +836,14 @@ describe('runestone', () => { }, ], Some( - new Etching(MAX_DIVISIBILITY, Some(new Rune(u128.MAX)), 0, None, None) + new Etching( + Some(MAX_DIVISIBILITY), + Some(new Rune(u128.MAX)), + None, + None, + None, + None + ) ), 32 ); @@ -828,7 +857,14 @@ describe('runestone', () => { }, ], Some( - new Etching(MAX_DIVISIBILITY, Some(new Rune(u128.MAX)), 0, None, None) + new Etching( + Some(MAX_DIVISIBILITY), + Some(new Rune(u128.MAX)), + None, + None, + None, + None + ) ), 50 ); @@ -1008,7 +1044,9 @@ describe('runestone', () => { const txnEtching = txnRunestone.etching.unwrap(); const etching = runestone.etching.unwrap(); - expect(txnEtching.divisibility).toBe(etching.divisibility); + expect(txnEtching.divisibility.unwrapOr(Infinity)).toBe( + etching.divisibility.unwrapOr(Infinity) + ); expect(txnEtching.mint.isSome()).toBe(etching.mint.isSome()); if (txnEtching.mint.isSome()) { const txnMint = txnEtching.mint.unwrap(); @@ -1028,12 +1066,19 @@ describe('runestone', () => { if (txnMint.term.isSome()) { expect(txnMint.term.unwrap()).toBe(mint.term.unwrap()); } + + expect(txnMint.cap.isSome()).toBe(mint.cap.isSome()); + if (txnMint.cap.isSome()) { + expect(txnMint.cap.unwrap()).toBe(mint.cap.unwrap()); + } } expect( txnEtching.rune.map((value) => value.toString()).unwrapOr('') ).toBe(etching.rune.map((value) => value.toString()).unwrapOr('')); - expect(txnEtching.spacers).toBe(etching.spacers); + expect(txnEtching.spacers.unwrapOr(Infinity)).toBe( + etching.spacers.unwrapOr(Infinity) + ); expect(txnEtching.symbol.unwrapOr('')).toBe( etching.symbol.unwrapOr('') ); @@ -1061,15 +1106,17 @@ describe('runestone', () => { ], Some( new Etching( - 1, + Some(1), Some(new Rune(u128(4))), - 6, + Some(6), Some('@'), Some({ deadline: Some(2), limit: Some(u128(3)), term: Some(5), - }) + cap: None, + }), + None ) ) ), @@ -1116,7 +1163,7 @@ describe('runestone', () => { None, None, [], - Some(new Etching(0, Some(new Rune(u128(3))), 0, None, None)) + Some(new Etching(None, Some(new Rune(u128(3))), None, None, None, None)) ), [Tag.FLAGS, Flag.mask(Flag.ETCH), Tag.RUNE, 3] ); @@ -1127,7 +1174,7 @@ describe('runestone', () => { None, None, [], - Some(new Etching(0, None, 0, None, None)) + Some(new Etching(None, None, None, None, None, None)) ), [Tag.FLAGS, Flag.mask(Flag.ETCH)] ); @@ -1219,13 +1266,6 @@ describe('runestone', () => { ); }); - test('invalid_limit_produces_cenotaph', () => { - expect(decipher([Tag.LIMIT, u128.MAX].map(u128)).cenotaph).toBe(true); - expect( - decipher([Tag.LIMIT, 0xffffffff_ffffffffn + 1n].map(u128)).cenotaph - ).toBe(true); - }); - test('min_and_max_runes_are_not_cenotaphs', () => { expect(decipher([Tag.RUNE, 0].map(u128)).cenotaph).toBe(false); expect(decipher([Tag.RUNE, u128.MAX].map(u128)).cenotaph).toBe(false); @@ -1242,4 +1282,60 @@ describe('runestone', () => { test('invalid_term_produces_cenotaph', () => { expect(decipher([Tag.TERM, u128.MAX].map(u128)).cenotaph).toBe(true); }); + + test('invalid_supply_produces_cenotaph', () => { + expect( + decipher( + [ + Tag.FLAGS, + Flag.mask(Flag.ETCH | Flag.MINT), + Tag.CAP, + 1, + Tag.LIMIT, + u128.MAX, + ].map(u128) + ).cenotaph + ).toBe(false); + + expect( + decipher( + [ + Tag.FLAGS, + Flag.mask(Flag.ETCH | Flag.MINT), + Tag.CAP, + 2, + Tag.LIMIT, + u128.MAX, + ].map(u128) + ).cenotaph + ).toBe(true); + + expect( + decipher( + [ + Tag.FLAGS, + Flag.mask(Flag.ETCH | Flag.MINT), + Tag.CAP, + 2, + Tag.LIMIT, + u128.MAX / 2n + 1n, + ].map(u128) + ).cenotaph + ).toBe(true); + + expect( + decipher( + [ + Tag.FLAGS, + Flag.mask(Flag.ETCH | Flag.MINT), + Tag.PREMINE, + 1, + Tag.CAP, + 1, + Tag.LIMIT, + u128.MAX, + ].map(u128) + ).cenotaph + ).toBe(true); + }); });