@btc-vision/btc-runtime 1.9.1 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -4
- package/runtime/constants/Exports.ts +86 -0
- package/runtime/contracts/OP1155.ts +1042 -0
- package/runtime/contracts/OP20.ts +56 -37
- package/runtime/contracts/OP721.ts +882 -0
- package/runtime/contracts/OP_NET.ts +5 -0
- package/runtime/contracts/ReentrancyGuard.ts +136 -0
- package/runtime/contracts/interfaces/IOP1155.ts +33 -0
- package/runtime/contracts/interfaces/IOP721.ts +29 -0
- package/runtime/contracts/interfaces/OP1155InitParameters.ts +11 -0
- package/runtime/contracts/interfaces/OP721InitParameters.ts +15 -0
- package/runtime/env/BlockchainEnvironment.ts +42 -3
- package/runtime/events/predefined/ApprovedForAll.ts +16 -0
- package/runtime/events/predefined/TransferredBatchEvent.ts +35 -0
- package/runtime/events/predefined/TransferredSingleEvent.ts +18 -0
- package/runtime/events/predefined/URIEvent.ts +23 -0
- package/runtime/events/predefined/index.ts +4 -0
- package/runtime/index.ts +9 -0
- package/runtime/math/abi.ts +23 -0
- package/runtime/math/bytes.ts +4 -0
- package/runtime/memory/AddressMemoryMap.ts +20 -0
- package/runtime/nested/storage/StorageMap.ts +3 -23
- package/runtime/nested/storage/StorageSet.ts +6 -3
- package/runtime/script/Script.ts +1 -1
- package/runtime/secp256k1/ECPoint.ts +3 -3
- package/runtime/shared-libraries/OP20Utils.ts +1 -2
- package/runtime/storage/AdvancedStoredString.ts +8 -189
- package/runtime/storage/BaseStoredString.ts +206 -0
- package/runtime/storage/StoredString.ts +15 -194
- package/runtime/storage/arrays/StoredPackedArray.ts +13 -4
- package/runtime/types/SafeMath.ts +125 -94
|
@@ -41,7 +41,7 @@ export class ECPoint {
|
|
|
41
41
|
// x3 = λ^2 - 2x mod P
|
|
42
42
|
// y3 = λ*(x - x3) - y mod P
|
|
43
43
|
// ----------------------------
|
|
44
|
-
static double(p: ECPoint): ECPoint {
|
|
44
|
+
public static double(p: ECPoint): ECPoint {
|
|
45
45
|
// If y=0, return infinity
|
|
46
46
|
if (p.y == u256.Zero) {
|
|
47
47
|
return new ECPoint(u256.Zero, u256.Zero); // "Point at infinity" convention
|
|
@@ -80,7 +80,7 @@ export class ECPoint {
|
|
|
80
80
|
// x3 = λ^2 - x1 - x2 mod P
|
|
81
81
|
// y3 = λ*(x1 - x3) - y1 mod P
|
|
82
82
|
// ----------------------------
|
|
83
|
-
static add(p: ECPoint, q: ECPoint): ECPoint {
|
|
83
|
+
public static add(p: ECPoint, q: ECPoint): ECPoint {
|
|
84
84
|
// 1) Check for infinity cases
|
|
85
85
|
const isPInfinity = p.x.isZero() && p.y.isZero();
|
|
86
86
|
const isQInfinity = q.x.isZero() && q.y.isZero();
|
|
@@ -122,7 +122,7 @@ export class ECPoint {
|
|
|
122
122
|
// Scalar Multiplication: k*P
|
|
123
123
|
// Double-and-add approach
|
|
124
124
|
// ----------------------------
|
|
125
|
-
static scalarMultiply(p: ECPoint, k: u256): ECPoint {
|
|
125
|
+
public static scalarMultiply(p: ECPoint, k: u256): ECPoint {
|
|
126
126
|
let result = new ECPoint(u256.Zero, u256.Zero); // ∞
|
|
127
127
|
let addend = p;
|
|
128
128
|
const two = u256.fromU64(2);
|
|
@@ -1,199 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { encodePointer } from '../math/abi';
|
|
3
|
-
import { bigEndianAdd } from '../math/bytes';
|
|
4
|
-
import { Revert } from '../types/Revert';
|
|
5
|
-
|
|
6
|
-
const MAX_LENGTH: u32 = 256;
|
|
1
|
+
import { BaseStoredString } from './BaseStoredString';
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* @class AdvancedStoredString
|
|
10
5
|
* @description
|
|
11
|
-
* Stores a string
|
|
12
|
-
*
|
|
13
|
-
* - Slot N>0: 32 bytes of data each
|
|
14
|
-
*
|
|
15
|
-
* The maximum is 65,535 bytes in UTF-8 form (not necessarily the same as code points).
|
|
6
|
+
* Stores a string with a directly provided subPointer.
|
|
7
|
+
* Maximum length: 256 bytes (configurable)
|
|
16
8
|
*/
|
|
17
9
|
@final
|
|
18
|
-
export class AdvancedStoredString {
|
|
19
|
-
constructor(
|
|
20
|
-
|
|
21
|
-
private readonly subPointer: Uint8Array,
|
|
22
|
-
) {}
|
|
23
|
-
|
|
24
|
-
private _value: string = '';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Cached string value. If `_value` is empty, we call `load()` on first access.
|
|
28
|
-
*/
|
|
29
|
-
public get value(): string {
|
|
30
|
-
if (!this._value) {
|
|
31
|
-
this.load();
|
|
32
|
-
}
|
|
33
|
-
return this._value;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
public set value(v: string) {
|
|
37
|
-
this._value = v;
|
|
38
|
-
|
|
39
|
-
this.save();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Derives a 32-byte pointer for the given chunkIndex and performs big-endian addition.
|
|
44
|
-
* chunkIndex=0 => header slot, 1 => second slot, etc.
|
|
45
|
-
*/
|
|
46
|
-
private getPointer(chunkIndex: u64): Uint8Array {
|
|
47
|
-
const base = encodePointer(this.pointer, this.subPointer, true, 'AdvancedStoredString');
|
|
48
|
-
return bigEndianAdd(base, chunkIndex);
|
|
10
|
+
export class AdvancedStoredString extends BaseStoredString {
|
|
11
|
+
constructor(pointer: u16, subPointer: Uint8Array, maxLength: u32 = 256) {
|
|
12
|
+
super(pointer, subPointer, maxLength);
|
|
49
13
|
}
|
|
50
14
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* Returns 0 if the slot is all zero.
|
|
54
|
-
*/
|
|
55
|
-
private getStoredLength(): u32 {
|
|
56
|
-
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
57
|
-
const b0 = <u32>headerSlot[0];
|
|
58
|
-
const b1 = <u32>headerSlot[1];
|
|
59
|
-
const b2 = <u32>headerSlot[2];
|
|
60
|
-
const b3 = <u32>headerSlot[3];
|
|
61
|
-
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Clears old data from storage. Based on `oldLength`, determines how many slots
|
|
66
|
-
* were used, and writes zeroed 32-byte arrays to each.
|
|
67
|
-
*/
|
|
68
|
-
private clearOldStorage(oldLength: u32): void {
|
|
69
|
-
if (oldLength == 0) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// We always use at least 1 slot (the header slot).
|
|
74
|
-
let chunkCount: u64 = 1;
|
|
75
|
-
|
|
76
|
-
// In the header slot, we can store up to 28 bytes of data.
|
|
77
|
-
const remaining = oldLength > 28 ? oldLength - 28 : 0;
|
|
78
|
-
if (remaining > 0) {
|
|
79
|
-
// Each additional chunk is 32 bytes.
|
|
80
|
-
// Use integer math ceiling: (remaining + 32 - 1) / 32
|
|
81
|
-
chunkCount += (remaining + 32 - 1) / 32;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Zero out each previously used slot
|
|
85
|
-
for (let i: u64 = 0; i < chunkCount; i++) {
|
|
86
|
-
Blockchain.setStorageAt(this.getPointer(i), new Uint8Array(32));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Saves the current string to storage in UTF-8 form.
|
|
92
|
-
*/
|
|
93
|
-
private save(): void {
|
|
94
|
-
// 1) Clear old data
|
|
95
|
-
const oldLen = this.getStoredLength();
|
|
96
|
-
this.clearOldStorage(oldLen);
|
|
97
|
-
|
|
98
|
-
// 2) Encode new string as UTF-8
|
|
99
|
-
const utf8Data = String.UTF8.encode(this._value, false);
|
|
100
|
-
const length = <u32>utf8Data.byteLength;
|
|
101
|
-
|
|
102
|
-
// Enforce max length
|
|
103
|
-
if (length > MAX_LENGTH) {
|
|
104
|
-
throw new Revert(`StoredString: value is too long (max=${MAX_LENGTH})`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 3) If new string is empty, just store a zeroed header and return
|
|
108
|
-
if (length == 0) {
|
|
109
|
-
// A zeroed 32-byte array => indicates length=0
|
|
110
|
-
Blockchain.setStorageAt(this.getPointer(0), new Uint8Array(32));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 4) Write the first slot: length + up to 28 bytes
|
|
115
|
-
let remaining: u32 = length;
|
|
116
|
-
let offset: u32 = 0;
|
|
117
|
-
const firstSlot = new Uint8Array(32);
|
|
118
|
-
firstSlot[0] = <u8>((length >> 24) & 0xff);
|
|
119
|
-
firstSlot[1] = <u8>((length >> 16) & 0xff);
|
|
120
|
-
firstSlot[2] = <u8>((length >> 8) & 0xff);
|
|
121
|
-
firstSlot[3] = <u8>(length & 0xff);
|
|
122
|
-
|
|
123
|
-
const bytes = Uint8Array.wrap(utf8Data);
|
|
124
|
-
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
125
|
-
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
126
|
-
firstSlot[4 + i] = bytes[i];
|
|
127
|
-
}
|
|
128
|
-
Blockchain.setStorageAt(this.getPointer(0), firstSlot);
|
|
129
|
-
|
|
130
|
-
remaining -= firstChunkSize;
|
|
131
|
-
offset += firstChunkSize;
|
|
132
|
-
|
|
133
|
-
// 5) Write subsequent slots (32 bytes each)
|
|
134
|
-
let chunkIndex: u64 = 1;
|
|
135
|
-
while (remaining > 0) {
|
|
136
|
-
const slotData = new Uint8Array(32);
|
|
137
|
-
const chunkSize = remaining < u32(32) ? remaining : u32(32);
|
|
138
|
-
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
139
|
-
slotData[i] = bytes[offset + i];
|
|
140
|
-
}
|
|
141
|
-
Blockchain.setStorageAt(this.getPointer(chunkIndex), slotData);
|
|
142
|
-
|
|
143
|
-
remaining -= chunkSize;
|
|
144
|
-
offset += chunkSize;
|
|
145
|
-
chunkIndex++;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Loads the string from storage by reading the stored byte length, then decoding
|
|
151
|
-
* the corresponding UTF-8 data from the slots.
|
|
152
|
-
*/
|
|
153
|
-
private load(): void {
|
|
154
|
-
// Read the header slot first
|
|
155
|
-
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
156
|
-
|
|
157
|
-
// Parse the big-endian length
|
|
158
|
-
const b0 = <u32>headerSlot[0];
|
|
159
|
-
const b1 = <u32>headerSlot[1];
|
|
160
|
-
const b2 = <u32>headerSlot[2];
|
|
161
|
-
const b3 = <u32>headerSlot[3];
|
|
162
|
-
const length: u32 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
163
|
-
|
|
164
|
-
// If length=0, then the string is empty
|
|
165
|
-
if (length == 0) {
|
|
166
|
-
this._value = '';
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Read the UTF-8 bytes from storage
|
|
171
|
-
let remaining: u32 = length;
|
|
172
|
-
let offset: u32 = 0;
|
|
173
|
-
const out = new Uint8Array(length);
|
|
174
|
-
|
|
175
|
-
// First slot can hold up to 28 bytes after the length
|
|
176
|
-
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
177
|
-
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
178
|
-
out[i] = headerSlot[4 + i];
|
|
179
|
-
}
|
|
180
|
-
remaining -= firstChunkSize;
|
|
181
|
-
offset += firstChunkSize;
|
|
182
|
-
|
|
183
|
-
// Read the subsequent slots of 32 bytes each
|
|
184
|
-
let chunkIndex: u64 = 1;
|
|
185
|
-
while (remaining > 0) {
|
|
186
|
-
const slotData = Blockchain.getStorageAt(this.getPointer(chunkIndex));
|
|
187
|
-
const chunkSize = remaining < 32 ? remaining : 32;
|
|
188
|
-
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
189
|
-
out[offset + i] = slotData[i];
|
|
190
|
-
}
|
|
191
|
-
remaining -= chunkSize;
|
|
192
|
-
offset += chunkSize;
|
|
193
|
-
chunkIndex++;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Decode UTF-8 into a normal string
|
|
197
|
-
this._value = String.UTF8.decode(out.buffer, false);
|
|
15
|
+
protected getClassName(): string {
|
|
16
|
+
return 'AdvancedStoredString';
|
|
198
17
|
}
|
|
199
18
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Blockchain } from '../env';
|
|
2
|
+
import { encodePointer } from '../math/abi';
|
|
3
|
+
import { bigEndianAdd } from '../math/bytes';
|
|
4
|
+
import { Revert } from '../types/Revert';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @class BaseStoredString
|
|
8
|
+
* @description
|
|
9
|
+
* Base class for storing a string in a sequence of 32-byte storage slots, in UTF-8 format:
|
|
10
|
+
* - Slot 0: first 4 bytes = length (big-endian), next 28 bytes = partial data
|
|
11
|
+
* - Slot N>0: 32 bytes of data each
|
|
12
|
+
*
|
|
13
|
+
* The maximum is configurable via MAX_LENGTH parameter (default: 256).
|
|
14
|
+
*/
|
|
15
|
+
export abstract class BaseStoredString {
|
|
16
|
+
protected readonly MAX_LENGTH: u32;
|
|
17
|
+
protected readonly subPointer: Uint8Array;
|
|
18
|
+
|
|
19
|
+
protected constructor(
|
|
20
|
+
public pointer: u16,
|
|
21
|
+
subPointer: Uint8Array,
|
|
22
|
+
maxLength: u32 = 256,
|
|
23
|
+
) {
|
|
24
|
+
this.subPointer = subPointer;
|
|
25
|
+
this.MAX_LENGTH = maxLength;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected _value: string = '';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Cached string value. If `_value` is empty, we call `load()` on first access.
|
|
32
|
+
*/
|
|
33
|
+
public get value(): string {
|
|
34
|
+
if (!this._value) {
|
|
35
|
+
this.load();
|
|
36
|
+
}
|
|
37
|
+
return this._value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public set value(v: string) {
|
|
41
|
+
this._value = v;
|
|
42
|
+
this.save();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derives a 32-byte pointer for the given chunkIndex and performs big-endian addition.
|
|
47
|
+
* chunkIndex=0 => header slot, 1 => second slot, etc.
|
|
48
|
+
*/
|
|
49
|
+
protected getPointer(chunkIndex: u64): Uint8Array {
|
|
50
|
+
const className = this.getClassName();
|
|
51
|
+
const base = encodePointer(this.pointer, this.subPointer, true, className);
|
|
52
|
+
return bigEndianAdd(base, chunkIndex);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns the class name for error messages - override in subclasses
|
|
57
|
+
*/
|
|
58
|
+
protected abstract getClassName(): string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reads the first slot and returns the stored byte length (big-endian).
|
|
62
|
+
* Returns 0 if the slot is all zero.
|
|
63
|
+
*/
|
|
64
|
+
protected getStoredLength(): u32 {
|
|
65
|
+
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
66
|
+
const b0 = <u32>headerSlot[0];
|
|
67
|
+
const b1 = <u32>headerSlot[1];
|
|
68
|
+
const b2 = <u32>headerSlot[2];
|
|
69
|
+
const b3 = <u32>headerSlot[3];
|
|
70
|
+
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clears old data from storage. Based on `oldLength`, determines how many slots
|
|
75
|
+
* were used, and writes zeroed 32-byte arrays to each.
|
|
76
|
+
*/
|
|
77
|
+
protected clearOldStorage(oldLength: u32): void {
|
|
78
|
+
if (oldLength == 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// We always use at least 1 slot (the header slot).
|
|
83
|
+
let chunkCount: u64 = 1;
|
|
84
|
+
|
|
85
|
+
// In the header slot, we can store up to 28 bytes of data.
|
|
86
|
+
const remaining = oldLength > 28 ? oldLength - 28 : 0;
|
|
87
|
+
if (remaining > 0) {
|
|
88
|
+
// Each additional chunk is 32 bytes.
|
|
89
|
+
// Use integer math ceiling: (remaining + 32 - 1) / 32
|
|
90
|
+
chunkCount += (remaining + 32 - 1) / 32;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Zero out each previously used slot
|
|
94
|
+
for (let i: u64 = 0; i < chunkCount; i++) {
|
|
95
|
+
Blockchain.setStorageAt(this.getPointer(i), new Uint8Array(32));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Saves the current string to storage in UTF-8 form.
|
|
101
|
+
*/
|
|
102
|
+
protected save(): void {
|
|
103
|
+
const oldLen = this.getStoredLength();
|
|
104
|
+
this.clearOldStorage(oldLen);
|
|
105
|
+
|
|
106
|
+
const utf8Data = String.UTF8.encode(this._value, false);
|
|
107
|
+
const length = <u32>utf8Data.byteLength;
|
|
108
|
+
|
|
109
|
+
// Enforce max length
|
|
110
|
+
if (length > this.MAX_LENGTH) {
|
|
111
|
+
throw new Revert(`${this.getClassName()}: value is too long (max=${this.MAX_LENGTH})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If new string is empty, just store a zeroed header and return
|
|
115
|
+
if (length == 0) {
|
|
116
|
+
// A zeroed 32-byte array => indicates length=0
|
|
117
|
+
Blockchain.setStorageAt(this.getPointer(0), new Uint8Array(32));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Write the first slot: length + up to 28 bytes
|
|
122
|
+
let remaining: u32 = length;
|
|
123
|
+
let offset: u32 = 0;
|
|
124
|
+
const firstSlot = new Uint8Array(32);
|
|
125
|
+
firstSlot[0] = <u8>((length >> 24) & 0xff);
|
|
126
|
+
firstSlot[1] = <u8>((length >> 16) & 0xff);
|
|
127
|
+
firstSlot[2] = <u8>((length >> 8) & 0xff);
|
|
128
|
+
firstSlot[3] = <u8>(length & 0xff);
|
|
129
|
+
|
|
130
|
+
const bytes = Uint8Array.wrap(utf8Data);
|
|
131
|
+
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
132
|
+
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
133
|
+
firstSlot[4 + i] = bytes[i];
|
|
134
|
+
}
|
|
135
|
+
Blockchain.setStorageAt(this.getPointer(0), firstSlot);
|
|
136
|
+
|
|
137
|
+
remaining -= firstChunkSize;
|
|
138
|
+
offset += firstChunkSize;
|
|
139
|
+
|
|
140
|
+
// Write subsequent slots (32 bytes each)
|
|
141
|
+
let chunkIndex: u64 = 1;
|
|
142
|
+
while (remaining > 0) {
|
|
143
|
+
const slotData = new Uint8Array(32);
|
|
144
|
+
const chunkSize = remaining < u32(32) ? remaining : u32(32);
|
|
145
|
+
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
146
|
+
slotData[i] = bytes[offset + i];
|
|
147
|
+
}
|
|
148
|
+
Blockchain.setStorageAt(this.getPointer(chunkIndex), slotData);
|
|
149
|
+
|
|
150
|
+
remaining -= chunkSize;
|
|
151
|
+
offset += chunkSize;
|
|
152
|
+
chunkIndex++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Loads the string from storage by reading the stored byte length, then decoding
|
|
158
|
+
* the corresponding UTF-8 data from the slots.
|
|
159
|
+
*/
|
|
160
|
+
protected load(): void {
|
|
161
|
+
// Read the header slot first
|
|
162
|
+
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
163
|
+
|
|
164
|
+
// Parse the big-endian length
|
|
165
|
+
const b0 = <u32>headerSlot[0];
|
|
166
|
+
const b1 = <u32>headerSlot[1];
|
|
167
|
+
const b2 = <u32>headerSlot[2];
|
|
168
|
+
const b3 = <u32>headerSlot[3];
|
|
169
|
+
const length: u32 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
170
|
+
|
|
171
|
+
// If length=0, then the string is empty
|
|
172
|
+
if (length == 0) {
|
|
173
|
+
this._value = '';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Read the UTF-8 bytes from storage
|
|
178
|
+
let remaining: u32 = length;
|
|
179
|
+
let offset: u32 = 0;
|
|
180
|
+
const out = new Uint8Array(length);
|
|
181
|
+
|
|
182
|
+
// First slot can hold up to 28 bytes after the length
|
|
183
|
+
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
184
|
+
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
185
|
+
out[i] = headerSlot[4 + i];
|
|
186
|
+
}
|
|
187
|
+
remaining -= firstChunkSize;
|
|
188
|
+
offset += firstChunkSize;
|
|
189
|
+
|
|
190
|
+
// Read the subsequent slots of 32 bytes each
|
|
191
|
+
let chunkIndex: u64 = 1;
|
|
192
|
+
while (remaining > 0) {
|
|
193
|
+
const slotData = Blockchain.getStorageAt(this.getPointer(chunkIndex));
|
|
194
|
+
const chunkSize = remaining < 32 ? remaining : 32;
|
|
195
|
+
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
196
|
+
out[offset + i] = slotData[i];
|
|
197
|
+
}
|
|
198
|
+
remaining -= chunkSize;
|
|
199
|
+
offset += chunkSize;
|
|
200
|
+
chunkIndex++;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Decode UTF-8 into a normal string
|
|
204
|
+
this._value = String.UTF8.decode(out.buffer, false);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -1,207 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { encodePointer } from '../math/abi';
|
|
3
|
-
import { bigEndianAdd } from '../math/bytes';
|
|
4
|
-
import { Revert } from '../types/Revert';
|
|
5
|
-
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
1
|
+
import { BaseStoredString } from './BaseStoredString';
|
|
6
2
|
import { SafeMath } from '../types/SafeMath';
|
|
7
|
-
|
|
8
|
-
const MAX_LENGTH = <u32>u16.MAX_VALUE;
|
|
9
|
-
const MAX_LENGTH_U256 = u256.fromU32(<u32>MAX_LENGTH);
|
|
3
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
6
|
* @class StoredString
|
|
13
7
|
* @description
|
|
14
|
-
* Stores a string
|
|
15
|
-
*
|
|
16
|
-
* - Slot N>0: 32 bytes of data each
|
|
17
|
-
*
|
|
18
|
-
* The maximum is 65,535 bytes in UTF-8 form (not necessarily the same as code points).
|
|
8
|
+
* Stores a string with an index-based subPointer calculation.
|
|
9
|
+
* Maximum length: 65,535 bytes (u16.MAX_VALUE)
|
|
19
10
|
*/
|
|
20
11
|
@final
|
|
21
|
-
export class StoredString {
|
|
22
|
-
private readonly
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
index: u64 = 0,
|
|
27
|
-
) {
|
|
28
|
-
const indexed = SafeMath.mul(u256.fromU64(index), MAX_LENGTH_U256);
|
|
29
|
-
this.subPointer = indexed.toUint8Array(true).slice(0, 30);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private _value: string = '';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Cached string value. If `_value` is empty, we call `load()` on first access.
|
|
36
|
-
*/
|
|
37
|
-
public get value(): string {
|
|
38
|
-
if (!this._value) {
|
|
39
|
-
this.load();
|
|
40
|
-
}
|
|
41
|
-
return this._value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
public set value(v: string) {
|
|
45
|
-
this._value = v;
|
|
46
|
-
|
|
47
|
-
this.save();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Derives a 32-byte pointer for the given chunkIndex and performs big-endian addition.
|
|
52
|
-
* chunkIndex=0 => header slot, 1 => second slot, etc.
|
|
53
|
-
*/
|
|
54
|
-
private getPointer(chunkIndex: u64): Uint8Array {
|
|
55
|
-
const base = encodePointer(this.pointer, this.subPointer, true, 'StoredString');
|
|
56
|
-
return bigEndianAdd(base, chunkIndex);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Reads the first slot and returns the stored byte length (big-endian).
|
|
61
|
-
* Returns 0 if the slot is all zero.
|
|
62
|
-
*/
|
|
63
|
-
private getStoredLength(): u32 {
|
|
64
|
-
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
65
|
-
const b0 = <u32>headerSlot[0];
|
|
66
|
-
const b1 = <u32>headerSlot[1];
|
|
67
|
-
const b2 = <u32>headerSlot[2];
|
|
68
|
-
const b3 = <u32>headerSlot[3];
|
|
69
|
-
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Clears old data from storage. Based on `oldLength`, determines how many slots
|
|
74
|
-
* were used, and writes zeroed 32-byte arrays to each.
|
|
75
|
-
*/
|
|
76
|
-
private clearOldStorage(oldLength: u32): void {
|
|
77
|
-
if (oldLength == 0) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// We always use at least 1 slot (the header slot).
|
|
82
|
-
let chunkCount: u64 = 1;
|
|
12
|
+
export class StoredString extends BaseStoredString {
|
|
13
|
+
private static readonly DEFAULT_MAX_LENGTH: u32 = <u32>u16.MAX_VALUE;
|
|
14
|
+
private static readonly MAX_LENGTH_U256: u256 = u256.fromU32(
|
|
15
|
+
<u32>StoredString.DEFAULT_MAX_LENGTH,
|
|
16
|
+
);
|
|
83
17
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
// Each additional chunk is 32 bytes.
|
|
88
|
-
// Use integer math ceiling: (remaining + 32 - 1) / 32
|
|
89
|
-
chunkCount += (remaining + 32 - 1) / 32;
|
|
90
|
-
}
|
|
18
|
+
constructor(pointer: u16, index: u64 = 0) {
|
|
19
|
+
const indexed = SafeMath.mul(u256.fromU64(index), StoredString.MAX_LENGTH_U256);
|
|
20
|
+
const subPointer = indexed.toUint8Array(true).slice(2, 32);
|
|
91
21
|
|
|
92
|
-
|
|
93
|
-
for (let i: u64 = 0; i < chunkCount; i++) {
|
|
94
|
-
Blockchain.setStorageAt(this.getPointer(i), new Uint8Array(32));
|
|
95
|
-
}
|
|
22
|
+
super(pointer, subPointer, StoredString.DEFAULT_MAX_LENGTH);
|
|
96
23
|
}
|
|
97
24
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*/
|
|
101
|
-
private save(): void {
|
|
102
|
-
// 1) Clear old data
|
|
103
|
-
const oldLen = this.getStoredLength();
|
|
104
|
-
this.clearOldStorage(oldLen);
|
|
105
|
-
|
|
106
|
-
// 2) Encode new string as UTF-8
|
|
107
|
-
const utf8Data = String.UTF8.encode(this._value, false);
|
|
108
|
-
const length = <u32>utf8Data.byteLength;
|
|
109
|
-
|
|
110
|
-
// Enforce max length
|
|
111
|
-
if (length > MAX_LENGTH) {
|
|
112
|
-
throw new Revert(`StoredString: value is too long (max=${MAX_LENGTH})`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 3) If new string is empty, just store a zeroed header and return
|
|
116
|
-
if (length == 0) {
|
|
117
|
-
// A zeroed 32-byte array => indicates length=0
|
|
118
|
-
Blockchain.setStorageAt(this.getPointer(0), new Uint8Array(32));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 4) Write the first slot: length + up to 28 bytes
|
|
123
|
-
let remaining: u32 = length;
|
|
124
|
-
let offset: u32 = 0;
|
|
125
|
-
const firstSlot = new Uint8Array(32);
|
|
126
|
-
firstSlot[0] = <u8>((length >> 24) & 0xff);
|
|
127
|
-
firstSlot[1] = <u8>((length >> 16) & 0xff);
|
|
128
|
-
firstSlot[2] = <u8>((length >> 8) & 0xff);
|
|
129
|
-
firstSlot[3] = <u8>(length & 0xff);
|
|
130
|
-
|
|
131
|
-
const bytes = Uint8Array.wrap(utf8Data);
|
|
132
|
-
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
133
|
-
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
134
|
-
firstSlot[4 + i] = bytes[i];
|
|
135
|
-
}
|
|
136
|
-
Blockchain.setStorageAt(this.getPointer(0), firstSlot);
|
|
137
|
-
|
|
138
|
-
remaining -= firstChunkSize;
|
|
139
|
-
offset += firstChunkSize;
|
|
140
|
-
|
|
141
|
-
// 5) Write subsequent slots (32 bytes each)
|
|
142
|
-
let chunkIndex: u64 = 1;
|
|
143
|
-
while (remaining > 0) {
|
|
144
|
-
const slotData = new Uint8Array(32);
|
|
145
|
-
const chunkSize = remaining < u32(32) ? remaining : u32(32);
|
|
146
|
-
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
147
|
-
slotData[i] = bytes[offset + i];
|
|
148
|
-
}
|
|
149
|
-
Blockchain.setStorageAt(this.getPointer(chunkIndex), slotData);
|
|
150
|
-
|
|
151
|
-
remaining -= chunkSize;
|
|
152
|
-
offset += chunkSize;
|
|
153
|
-
chunkIndex++;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Loads the string from storage by reading the stored byte length, then decoding
|
|
159
|
-
* the corresponding UTF-8 data from the slots.
|
|
160
|
-
*/
|
|
161
|
-
private load(): void {
|
|
162
|
-
// Read the header slot first
|
|
163
|
-
const headerSlot = Blockchain.getStorageAt(this.getPointer(0));
|
|
164
|
-
|
|
165
|
-
// Parse the big-endian length
|
|
166
|
-
const b0 = <u32>headerSlot[0];
|
|
167
|
-
const b1 = <u32>headerSlot[1];
|
|
168
|
-
const b2 = <u32>headerSlot[2];
|
|
169
|
-
const b3 = <u32>headerSlot[3];
|
|
170
|
-
const length: u32 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
|
|
171
|
-
|
|
172
|
-
// If length=0, then the string is empty
|
|
173
|
-
if (length == 0) {
|
|
174
|
-
this._value = '';
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Read the UTF-8 bytes from storage
|
|
179
|
-
let remaining: u32 = length;
|
|
180
|
-
let offset: u32 = 0;
|
|
181
|
-
const out = new Uint8Array(length);
|
|
182
|
-
|
|
183
|
-
// First slot can hold up to 28 bytes after the length
|
|
184
|
-
const firstChunkSize = remaining < 28 ? remaining : 28;
|
|
185
|
-
for (let i: u32 = 0; i < firstChunkSize; i++) {
|
|
186
|
-
out[i] = headerSlot[4 + i];
|
|
187
|
-
}
|
|
188
|
-
remaining -= firstChunkSize;
|
|
189
|
-
offset += firstChunkSize;
|
|
190
|
-
|
|
191
|
-
// Read the subsequent slots of 32 bytes each
|
|
192
|
-
let chunkIndex: u64 = 1;
|
|
193
|
-
while (remaining > 0) {
|
|
194
|
-
const slotData = Blockchain.getStorageAt(this.getPointer(chunkIndex));
|
|
195
|
-
const chunkSize = remaining < 32 ? remaining : 32;
|
|
196
|
-
for (let i: u32 = 0; i < chunkSize; i++) {
|
|
197
|
-
out[offset + i] = slotData[i];
|
|
198
|
-
}
|
|
199
|
-
remaining -= chunkSize;
|
|
200
|
-
offset += chunkSize;
|
|
201
|
-
chunkIndex++;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Decode UTF-8 into a normal string
|
|
205
|
-
this._value = String.UTF8.decode(out.buffer, false);
|
|
25
|
+
protected getClassName(): string {
|
|
26
|
+
return 'StoredString';
|
|
206
27
|
}
|
|
207
28
|
}
|