Skip to content

Commit

Permalink
Allow supply-capped mints (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
summraznboi authored Mar 28, 2024
1 parent 3405fee commit e755f41
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 71 deletions.
8 changes: 5 additions & 3 deletions src/etching.ts
Original file line number Diff line number Diff line change
@@ -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<string>;

constructor(
readonly divisibility: number,
readonly divisibility: Option<number>,
readonly rune: Option<Rune>,
readonly spacers: number,
readonly spacers: Option<number>,
symbol: Option<string>,
readonly mint: Option<Mint>
readonly mint: Option<Mint>,
readonly premine: Option<u128>
) {
this.symbol = symbol.andThen((value) => {
const codePoint = value.codePointAt(0);
Expand Down
7 changes: 4 additions & 3 deletions src/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Option } from '@sniptt/monads';
import { u128 } from './u128';

export type Mint = {
deadline: Option<number>;
limit: Option<u128>;
term: Option<number>;
cap: Option<u128>; // mint cap
deadline: Option<number>; // unix timestamp
limit: Option<u128>; // claim amount
term: Option<number>; // relative block height
};
17 changes: 15 additions & 2 deletions src/rune.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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];

Expand All @@ -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());
}
Expand Down
59 changes: 46 additions & 13 deletions src/runestone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ export class Runestone {
}
}

static cenotaph(): Runestone {
return new Runestone(true, None, None, [], None);
}

static decipher(transaction: bitcoin.Transaction): Option<Runestone> {
const optionPayload = Runestone.payload(transaction);
if (optionPayload.isNone()) {
return None;
}
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(
Expand Down Expand Up @@ -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,
Expand All @@ -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)) {
Expand All @@ -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]) =>
Expand All @@ -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<Etching> = etch
? Some(
new Etching(
Expand All @@ -162,18 +180,21 @@ export class Runestone {
symbol,
mint
? Some({
cap,
deadline,
limit,
term,
})
: None
: None,
premine
)
)
: None;

return Some(
new Runestone(
cenotaph ||
overflow ||
flags !== 0n ||
[...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined,
claim,
Expand Down Expand Up @@ -203,21 +224,28 @@ 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()) {
const symbol = etching.symbol.unwrap();
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();

Expand All @@ -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]));
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export enum Tag {
DEADLINE = 10,
DEFAULT_OUTPUT = 12,
CLAIM = 14,
CAP = 16,
PREMINE = 18,
CENOTAPH = 126,

DIVISIBILITY = 1,
Expand Down
2 changes: 1 addition & 1 deletion src/u128.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions test/rune.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
});
});
Loading

0 comments on commit e755f41

Please sign in to comment.