1 | import {
|
2 | AssetJSON,
|
3 | BinaryWriter,
|
4 | common,
|
5 | crypto,
|
6 | ECPoint,
|
7 | IOHelper,
|
8 | JSONHelper,
|
9 | UInt160,
|
10 | UInt256,
|
11 | UInt256Hex,
|
12 | } from '@neo-one/client-common-esnext-esm';
|
13 | import { BaseState } from '@neo-one/client-full-common-esnext-esm';
|
14 | import { makeErrorWithCode } from '@neo-one/utils-esnext-esm';
|
15 | import BN from 'bn.js';
|
16 | import { assertAssetType, AssetType, toJSONAssetType } from './AssetType';
|
17 | import { Equals, EquatableKey } from './Equatable';
|
18 | import {
|
19 | createSerializeWire,
|
20 | DeserializeWireBaseOptions,
|
21 | DeserializeWireOptions,
|
22 | SerializableJSON,
|
23 | SerializableWire,
|
24 | SerializeJSONContext,
|
25 | SerializeWire,
|
26 | } from './Serializable';
|
27 | import { BinaryReader, utils } from './utils';
|
28 |
|
29 | export const InvalidAssetError = makeErrorWithCode('INVALID_ASSET', (message: string) => message);
|
30 |
|
31 | export interface AssetKey {
|
32 | readonly hash: UInt256;
|
33 | }
|
34 | export interface AssetAdd {
|
35 | readonly version?: number;
|
36 | readonly hash: UInt256;
|
37 | readonly type: AssetType;
|
38 | readonly name: string;
|
39 | readonly amount: BN;
|
40 | readonly available?: BN;
|
41 | readonly precision: number;
|
42 | readonly feeMode?: number;
|
43 | readonly fee?: BN;
|
44 | readonly feeAddress?: UInt160;
|
45 | readonly owner: ECPoint;
|
46 | readonly admin: UInt160;
|
47 | readonly issuer: UInt160;
|
48 | readonly expiration: number;
|
49 | readonly isFrozen?: boolean;
|
50 | }
|
51 |
|
52 | export interface AssetUpdate {
|
53 | readonly available?: BN;
|
54 | readonly expiration?: number;
|
55 | readonly isFrozen?: boolean;
|
56 | }
|
57 |
|
58 | const NAME_MAX_LENGTH = 1024;
|
59 | const PRECISION_MAX = 8;
|
60 |
|
61 | export class Asset extends BaseState implements SerializableWire<Asset>, SerializableJSON<AssetJSON>, EquatableKey {
|
62 | public static deserializeWireBase({ reader }: DeserializeWireBaseOptions): Asset {
|
63 | const version = reader.readUInt8();
|
64 | const hash = reader.readUInt256();
|
65 | const type = assertAssetType(reader.readUInt8());
|
66 | const name = reader.readVarString();
|
67 | const amount = reader.readFixed8();
|
68 | const available = reader.readFixed8();
|
69 | const precision = reader.readUInt8();
|
70 | reader.readUInt8();
|
71 | const fee = reader.readFixed8();
|
72 | const feeAddress = reader.readUInt160();
|
73 | const owner = reader.readECPoint();
|
74 | const admin = reader.readUInt160();
|
75 | const issuer = reader.readUInt160();
|
76 | const expiration = reader.readUInt32LE();
|
77 | const isFrozen = reader.readBoolean();
|
78 |
|
79 | return new Asset({
|
80 | version,
|
81 | hash,
|
82 | type,
|
83 | name,
|
84 | amount,
|
85 | available,
|
86 | precision,
|
87 | fee,
|
88 | feeAddress,
|
89 | owner,
|
90 | admin,
|
91 | issuer,
|
92 | expiration,
|
93 | isFrozen,
|
94 | });
|
95 | }
|
96 |
|
97 | public static deserializeWire(options: DeserializeWireOptions): Asset {
|
98 | return this.deserializeWireBase({
|
99 | context: options.context,
|
100 | reader: new BinaryReader(options.buffer),
|
101 | });
|
102 | }
|
103 |
|
104 | public readonly hash: UInt256;
|
105 | public readonly hashHex: UInt256Hex;
|
106 | public readonly type: AssetType;
|
107 | public readonly name: string;
|
108 | public readonly amount: BN;
|
109 | public readonly available: BN;
|
110 | public readonly precision: number;
|
111 | public readonly feeMode: number;
|
112 | public readonly fee: BN;
|
113 | public readonly feeAddress: UInt160;
|
114 | public readonly owner: ECPoint;
|
115 | public readonly admin: UInt160;
|
116 | public readonly issuer: UInt160;
|
117 | public readonly expiration: number;
|
118 | public readonly isFrozen: boolean;
|
119 | public readonly equals: Equals = utils.equals(Asset, this, (other) => common.uInt256Equal(this.hash, other.hash));
|
120 | public readonly toKeyString = utils.toKeyString(Asset, () => this.hashHex);
|
121 | public readonly serializeWire: SerializeWire = createSerializeWire(this.serializeWireBase.bind(this));
|
122 | private readonly sizeInternal: () => number;
|
123 |
|
124 | public constructor({
|
125 | version,
|
126 | hash,
|
127 | type,
|
128 | name,
|
129 | amount,
|
130 | available = utils.ZERO,
|
131 | precision,
|
132 | feeMode = 0,
|
133 | fee = utils.ZERO,
|
134 | feeAddress = common.ZERO_UINT160,
|
135 | owner,
|
136 | admin,
|
137 | issuer,
|
138 | expiration,
|
139 | isFrozen = false,
|
140 | }: AssetAdd) {
|
141 | super({ version });
|
142 |
|
143 | verifyAsset({ name, type, amount, precision });
|
144 | this.hash = hash;
|
145 | this.hashHex = common.uInt256ToHex(hash);
|
146 | this.type = type;
|
147 | this.name = name;
|
148 | this.amount = amount;
|
149 | this.available = available;
|
150 | this.precision = precision;
|
151 | this.feeMode = feeMode;
|
152 | this.fee = fee;
|
153 | this.feeAddress = feeAddress;
|
154 | this.owner = owner;
|
155 | this.admin = admin;
|
156 | this.issuer = issuer;
|
157 | this.expiration = expiration;
|
158 | this.isFrozen = isFrozen;
|
159 | this.sizeInternal = utils.lazy(
|
160 | () =>
|
161 | IOHelper.sizeOfUInt8 +
|
162 | IOHelper.sizeOfUInt256 +
|
163 | IOHelper.sizeOfUInt8 +
|
164 | IOHelper.sizeOfVarString(this.name) +
|
165 | IOHelper.sizeOfFixed8 +
|
166 | IOHelper.sizeOfFixed8 +
|
167 | IOHelper.sizeOfUInt8 +
|
168 | IOHelper.sizeOfUInt8 +
|
169 | IOHelper.sizeOfFixed8 +
|
170 | IOHelper.sizeOfUInt160 +
|
171 | IOHelper.sizeOfECPoint(this.owner) +
|
172 | IOHelper.sizeOfUInt160 +
|
173 | IOHelper.sizeOfUInt160 +
|
174 | IOHelper.sizeOfUInt32LE +
|
175 | IOHelper.sizeOfBoolean,
|
176 | );
|
177 | }
|
178 | public get size(): number {
|
179 | return this.sizeInternal();
|
180 | }
|
181 |
|
182 | public update({
|
183 | available = this.available,
|
184 | expiration = this.expiration,
|
185 | isFrozen = this.isFrozen,
|
186 | }: AssetUpdate): Asset {
|
187 | return new Asset({
|
188 | hash: this.hash,
|
189 | type: this.type,
|
190 | name: this.name,
|
191 | amount: this.amount,
|
192 | precision: this.precision,
|
193 | fee: this.fee,
|
194 | feeAddress: this.feeAddress,
|
195 | owner: this.owner,
|
196 | admin: this.admin,
|
197 | issuer: this.issuer,
|
198 | available,
|
199 | expiration,
|
200 | isFrozen,
|
201 | });
|
202 | }
|
203 |
|
204 | public serializeWireBase(writer: BinaryWriter): void {
|
205 | writer.writeUInt8(this.version);
|
206 | writer.writeUInt256(this.hash);
|
207 | writer.writeUInt8(this.type);
|
208 | writer.writeVarString(this.name);
|
209 | writer.writeFixed8(this.amount);
|
210 | writer.writeFixed8(this.available);
|
211 | writer.writeUInt8(this.precision);
|
212 | writer.writeUInt8(this.feeMode);
|
213 | writer.writeFixed8(this.fee);
|
214 | writer.writeUInt160(this.feeAddress);
|
215 | writer.writeECPoint(this.owner);
|
216 | writer.writeUInt160(this.admin);
|
217 | writer.writeUInt160(this.issuer);
|
218 | writer.writeUInt32LE(this.expiration);
|
219 | writer.writeBoolean(this.isFrozen);
|
220 | }
|
221 |
|
222 | public serializeJSON(context: SerializeJSONContext): AssetJSON {
|
223 | let name = this.name;
|
224 | try {
|
225 | name = JSON.parse(name);
|
226 | } catch {
|
227 |
|
228 | }
|
229 |
|
230 | return {
|
231 | version: this.version,
|
232 | id: JSONHelper.writeUInt256(this.hash),
|
233 | type: toJSONAssetType(this.type),
|
234 | name,
|
235 | amount: JSONHelper.writeFixed8(this.amount),
|
236 | available: JSONHelper.writeFixed8(this.available),
|
237 | precision: this.precision,
|
238 | owner: JSONHelper.writeECPoint(this.owner),
|
239 | admin: crypto.scriptHashToAddress({
|
240 | addressVersion: context.addressVersion,
|
241 | scriptHash: this.admin,
|
242 | }),
|
243 |
|
244 | issuer: crypto.scriptHashToAddress({
|
245 | addressVersion: context.addressVersion,
|
246 | scriptHash: this.issuer,
|
247 | }),
|
248 |
|
249 | expiration: this.expiration,
|
250 | frozen: this.isFrozen,
|
251 | };
|
252 | }
|
253 | }
|
254 |
|
255 | export const verifyAsset = ({
|
256 | name,
|
257 | type,
|
258 | amount,
|
259 | precision,
|
260 | }: {
|
261 | readonly name: AssetAdd['name'];
|
262 | readonly type: AssetAdd['type'];
|
263 | readonly amount: AssetAdd['amount'];
|
264 | readonly precision: AssetAdd['precision'];
|
265 | }) => {
|
266 | if (type === AssetType.CreditFlag || type === AssetType.DutyFlag) {
|
267 | throw new InvalidAssetError(`Asset type cannot be CREDIT_FLAG or DUTY_FLAG, received: ${type}`);
|
268 | }
|
269 |
|
270 | const nameBuffer = Buffer.from(name, 'utf8');
|
271 | if (nameBuffer.length > NAME_MAX_LENGTH) {
|
272 | throw new InvalidAssetError(`Name too long. Max: ${NAME_MAX_LENGTH}, Received: ${nameBuffer.length}`);
|
273 | }
|
274 |
|
275 | if (amount.lte(utils.ZERO) && !amount.eq(common.NEGATIVE_SATOSHI_FIXED8)) {
|
276 | throw new InvalidAssetError(`Amount must be greater than 0. (received ${amount})`);
|
277 | }
|
278 |
|
279 | if (type === AssetType.Invoice && !amount.eq(common.NEGATIVE_SATOSHI_FIXED8)) {
|
280 | throw new InvalidAssetError('Invoice assets must have unlimited amount.');
|
281 | }
|
282 |
|
283 | if (precision > PRECISION_MAX) {
|
284 | throw new InvalidAssetError(`Max precision is 8. Received: ${precision}`);
|
285 | }
|
286 |
|
287 | if (!amount.eq(utils.NEGATIVE_SATOSHI) && !amount.mod(utils.TEN.pow(utils.EIGHT.subn(precision))).eq(utils.ZERO)) {
|
288 | throw new InvalidAssetError('Invalid precision for amount.');
|
289 | }
|
290 | };
|