@alephium/web3 0.1.0 → 0.2.0-rc.10

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.
Files changed (93) hide show
  1. package/.eslintignore +2 -2
  2. package/README.md +2 -135
  3. package/contracts/greeter/greeter.ral +7 -0
  4. package/contracts/{greeter_interface.ral → greeter/greeter_interface.ral} +1 -0
  5. package/contracts/greeter_main.ral +2 -4
  6. package/dist/alephium-web3.min.js +1 -1
  7. package/dist/alephium-web3.min.js.LICENSE.txt +0 -17
  8. package/dist/alephium-web3.min.js.map +1 -1
  9. package/dist/scripts/create-project.js +3 -2
  10. package/dist/src/api/api-alephium.d.ts +58 -13
  11. package/dist/src/api/api-alephium.js +19 -3
  12. package/dist/src/api/api-explorer.d.ts +32 -16
  13. package/dist/src/api/api-explorer.js +26 -0
  14. package/dist/src/api/index.d.ts +2 -2
  15. package/dist/src/api/index.js +18 -3
  16. package/dist/src/contract/contract.d.ts +105 -48
  17. package/dist/src/contract/contract.js +362 -189
  18. package/dist/src/contract/events.d.ts +7 -25
  19. package/dist/src/contract/events.js +18 -31
  20. package/dist/src/contract/index.js +5 -1
  21. package/dist/src/global.d.ts +4 -0
  22. package/dist/src/global.js +38 -0
  23. package/dist/src/index.d.ts +3 -0
  24. package/dist/src/index.js +21 -1
  25. package/dist/src/signer/index.d.ts +0 -1
  26. package/dist/src/signer/index.js +5 -2
  27. package/dist/src/signer/signer.d.ts +1 -1
  28. package/dist/src/signer/signer.js +8 -3
  29. package/dist/src/transaction/index.d.ts +1 -0
  30. package/dist/src/{test → transaction}/index.js +6 -13
  31. package/dist/src/transaction/status.d.ts +10 -0
  32. package/dist/src/transaction/status.js +48 -0
  33. package/dist/src/utils/index.d.ts +1 -2
  34. package/dist/src/utils/index.js +6 -3
  35. package/dist/src/utils/subscription.d.ts +23 -0
  36. package/dist/src/utils/subscription.js +53 -0
  37. package/dist/src/utils/utils.d.ts +5 -2
  38. package/dist/src/utils/utils.js +25 -16
  39. package/jest-config.json +11 -0
  40. package/package.json +7 -35
  41. package/scripts/create-project.ts +3 -2
  42. package/src/api/api-alephium.ts +73 -8
  43. package/src/api/api-explorer.ts +30 -0
  44. package/src/api/index.ts +13 -3
  45. package/src/contract/contract.ts +512 -279
  46. package/src/contract/events.ts +21 -48
  47. package/src/{utils/transaction.ts → global.ts} +14 -15
  48. package/src/index.ts +4 -0
  49. package/src/signer/index.ts +0 -1
  50. package/src/signer/signer.ts +4 -3
  51. package/src/{utils/password-crypto.test.ts → transaction/index.ts} +1 -9
  52. package/src/transaction/status.ts +58 -0
  53. package/src/utils/index.ts +1 -2
  54. package/src/utils/subscription.ts +72 -0
  55. package/src/utils/utils.ts +16 -8
  56. package/templates/base/package.json +2 -2
  57. package/templates/base/src/greeter.ts +8 -7
  58. package/templates/react/package.json +2 -2
  59. package/templates/react/src/App.tsx +2 -2
  60. package/.eslintrc.json +0 -21
  61. package/LICENSE +0 -165
  62. package/contracts/add.ral +0 -16
  63. package/contracts/greeter.ral +0 -7
  64. package/contracts/main.ral +0 -6
  65. package/contracts/sub.ral +0 -9
  66. package/dist/src/signer/node-wallet.d.ts +0 -13
  67. package/dist/src/signer/node-wallet.js +0 -60
  68. package/dist/src/test/index.d.ts +0 -7
  69. package/dist/src/test/privatekey-wallet.d.ts +0 -12
  70. package/dist/src/test/privatekey-wallet.js +0 -68
  71. package/dist/src/utils/password-crypto.d.ts +0 -2
  72. package/dist/src/utils/password-crypto.js +0 -69
  73. package/dist/src/utils/transaction.d.ts +0 -2
  74. package/dist/src/utils/transaction.js +0 -58
  75. package/gitignore +0 -10
  76. package/src/contract/ralph.test.ts +0 -178
  77. package/src/fixtures/address.json +0 -36
  78. package/src/fixtures/balance.json +0 -9
  79. package/src/fixtures/self-clique.json +0 -19
  80. package/src/fixtures/transaction.json +0 -13
  81. package/src/fixtures/transactions.json +0 -179
  82. package/src/signer/fixtures/genesis.json +0 -26
  83. package/src/signer/fixtures/wallets.json +0 -26
  84. package/src/signer/node-wallet.ts +0 -74
  85. package/src/test/index.ts +0 -32
  86. package/src/test/privatekey-wallet.ts +0 -58
  87. package/src/utils/address.test.ts +0 -47
  88. package/src/utils/djb2.test.ts +0 -35
  89. package/src/utils/password-crypto.ts +0 -77
  90. package/src/utils/transaction.test.ts +0 -50
  91. package/src/utils/utils.test.ts +0 -160
  92. package/test/contract.test.ts +0 -161
  93. package/test/events.test.ts +0 -139
@@ -17,207 +17,517 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
18
 
19
19
  import { Buffer } from 'buffer/'
20
- import * as cryptojs from 'crypto-js'
21
- import * as crypto from 'crypto'
20
+ import { webcrypto as crypto } from 'crypto'
22
21
  import fs from 'fs'
23
22
  import { promises as fsPromises } from 'fs'
24
- import { NodeProvider } from '../api'
25
- import { node } from '../api'
23
+ import { node, NodeProvider } from '../api'
26
24
  import { SignDeployContractTxParams, SignExecuteScriptTxParams, SignerWithNodeProvider } from '../signer'
27
25
  import * as ralph from './ralph'
28
26
  import { bs58, binToHex, contractIdFromAddress, assertType, Eq } from '../utils'
27
+ import { CompileContractResult, CompileScriptResult } from '../api/api-alephium'
28
+ import { getCurrentNodeProvider } from '../global'
29
+
30
+ type FieldsSig = node.FieldsSig
31
+ type EventSig = node.EventSig
32
+ type FunctionSig = node.FunctionSig
33
+
34
+ enum SourceType {
35
+ Contract = 0,
36
+ Script = 1,
37
+ AbstractContract = 2,
38
+ Interface = 3
39
+ }
29
40
 
30
- export abstract class Common {
31
- readonly sourceCodeSha256: string
32
- readonly functions: node.FunctionSig[]
33
-
34
- static readonly importRegex = new RegExp('^import "[a-z][a-z_0-9]*.ral"', 'mg')
35
- static readonly contractRegex = new RegExp('^TxContract [A-Z][a-zA-Z0-9]*', 'mg')
36
- static readonly interfaceRegex = new RegExp('^Interface [A-Z][a-zA-Z0-9]* \\{', 'mg')
37
- static readonly scriptRegex = new RegExp('^TxScript [A-Z][a-zA-Z0-9]*', 'mg')
38
-
39
- private static _artifactCache: Map<string, Contract | Script> = new Map<string, Contract | Script>()
40
- static artifactCacheCapacity = 20
41
- protected static _getArtifactFromCache(codeHash: string): Contract | Script | undefined {
42
- return this._artifactCache.get(codeHash)
43
- }
44
- protected static _putArtifactToCache(contract: Contract): void {
45
- if (!this._artifactCache.has(contract.codeHash)) {
46
- if (this._artifactCache.size >= this.artifactCacheCapacity) {
47
- const keyToDelete = this._artifactCache.keys().next().value
48
- this._artifactCache.delete(keyToDelete)
49
- }
50
- this._artifactCache.set(contract.codeHash, contract)
51
- }
41
+ export type CompilerOptions = node.CompilerOptions & {
42
+ errorOnWarnings: boolean
43
+ }
44
+
45
+ export const DEFAULT_NODE_COMPILER_OPTIONS: node.CompilerOptions = {
46
+ ignoreUnusedConstantsWarnings: false,
47
+ ignoreUnusedVariablesWarnings: false,
48
+ ignoreUnusedFieldsWarnings: false,
49
+ ignoreUnusedPrivateFunctionsWarnings: false,
50
+ ignoreReadonlyCheckWarnings: false,
51
+ ignoreExternalCallCheckWarnings: false
52
+ }
53
+
54
+ export const DEFAULT_COMPILER_OPTIONS: CompilerOptions = { errorOnWarnings: true, ...DEFAULT_NODE_COMPILER_OPTIONS }
55
+
56
+ class TypedMatcher<T extends SourceType> {
57
+ matcher: RegExp
58
+ type: T
59
+
60
+ constructor(pattern: string, type: T) {
61
+ this.matcher = new RegExp(pattern, 'mg')
62
+ this.type = type
52
63
  }
53
64
 
54
- constructor(sourceCodeSha256: string, functions: node.FunctionSig[]) {
55
- this.sourceCodeSha256 = sourceCodeSha256
56
- this.functions = functions
65
+ match(str: string): number {
66
+ const results = str.match(this.matcher)
67
+ return results === null ? 0 : results.length
68
+ }
69
+ }
70
+
71
+ class SourceFile {
72
+ type: SourceType
73
+ contractPath: string
74
+ sourceCode: string
75
+ sourceCodeHash: string
76
+
77
+ getArtifactPath(artifactsRootPath: string): string {
78
+ return artifactsRootPath + this.contractPath.slice(this.contractPath.indexOf('/')) + '.json'
57
79
  }
58
80
 
59
- protected static _contractPath(fileName: string): string {
60
- if (fileName.endsWith('.json')) {
61
- return `./contracts/${fileName.slice(0, -5)}`
62
- } else {
63
- return `./contracts/${fileName}`
81
+ constructor(type: SourceType, sourceCode: string, sourceCodeHash: string, contractPath: string) {
82
+ this.type = type
83
+ this.sourceCode = sourceCode
84
+ this.sourceCodeHash = sourceCodeHash
85
+ this.contractPath = contractPath
86
+ }
87
+
88
+ static async from(type: SourceType, sourceCode: string, contractPath: string): Promise<SourceFile> {
89
+ const sourceCodeHash = await crypto.subtle.digest('SHA-256', Buffer.from(sourceCode))
90
+ return new SourceFile(type, sourceCode, Buffer.from(sourceCodeHash).toString('hex'), contractPath)
91
+ }
92
+ }
93
+
94
+ class Compiled<T extends Artifact> {
95
+ sourceFile: SourceFile
96
+ artifact: T
97
+ warnings: string[]
98
+
99
+ constructor(sourceFile: SourceFile, artifact: T, warnings: string[]) {
100
+ this.sourceFile = sourceFile
101
+ this.artifact = artifact
102
+ this.warnings = warnings
103
+ }
104
+ }
105
+
106
+ class ProjectArtifact {
107
+ static readonly artifactFileName = '.project.json'
108
+
109
+ compilerOptionsUsed: node.CompilerOptions
110
+ infos: Map<string, { sourceCodeHash: string; warnings: string[] }>
111
+
112
+ static checkCompilerOptionsParameter(compilerOptions: node.CompilerOptions): void {
113
+ if (Object.keys(compilerOptions).length != Object.keys(DEFAULT_NODE_COMPILER_OPTIONS).length) {
114
+ throw Error(`Not all compiler options are set: ${compilerOptions}`)
115
+ }
116
+
117
+ const combined = { ...compilerOptions, ...DEFAULT_NODE_COMPILER_OPTIONS }
118
+ if (Object.keys(combined).length !== Object.keys(DEFAULT_NODE_COMPILER_OPTIONS).length) {
119
+ throw Error(`There are unknown compiler options: ${compilerOptions}`)
64
120
  }
65
121
  }
66
122
 
67
- protected static _artifactPath(fileName: string): string {
68
- if (fileName.endsWith('.json')) {
69
- return `./artifacts/${fileName}`
70
- } else {
71
- return `./artifacts/${fileName}.json`
123
+ constructor(
124
+ compilerOptionsUsed: node.CompilerOptions,
125
+ infos: Map<string, { sourceCodeHash: string; warnings: string[] }>
126
+ ) {
127
+ ProjectArtifact.checkCompilerOptionsParameter(compilerOptionsUsed)
128
+ this.compilerOptionsUsed = compilerOptionsUsed
129
+ this.infos = infos
130
+ }
131
+
132
+ async saveToFile(rootPath: string): Promise<void> {
133
+ const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
134
+ const artifact = { compilerOptionsUsed: this.compilerOptionsUsed, infos: Object.fromEntries(this.infos) }
135
+ const content = JSON.stringify(artifact, null, 2)
136
+ return fsPromises.writeFile(filepath, content)
137
+ }
138
+
139
+ needToReCompile(compilerOptions: node.CompilerOptions, files: SourceFile[]): boolean {
140
+ ProjectArtifact.checkCompilerOptionsParameter(compilerOptions)
141
+
142
+ const optionsMatched = Object.entries(compilerOptions).every(([key, inputOption]) => {
143
+ const usedOption = this.compilerOptionsUsed[`${key}`]
144
+ return usedOption === inputOption
145
+ })
146
+ if (!optionsMatched) {
147
+ return true
148
+ }
149
+
150
+ if (files.length !== this.infos.size) {
151
+ return true
72
152
  }
153
+ for (const file of files) {
154
+ const info = this.infos.get(file.contractPath)
155
+ if (typeof info === 'undefined' || info.sourceCodeHash !== file.sourceCodeHash) {
156
+ return true
157
+ }
158
+ }
159
+
160
+ return false
73
161
  }
74
162
 
75
- protected static _artifactsFolder(): string {
76
- return './artifacts/'
163
+ static async from(rootPath: string): Promise<ProjectArtifact | undefined> {
164
+ const filepath = rootPath + '/' + ProjectArtifact.artifactFileName
165
+ if (!fs.existsSync(filepath)) {
166
+ return undefined
167
+ }
168
+ const content = await fsPromises.readFile(filepath)
169
+ const json = JSON.parse(content.toString())
170
+ const compilerOptionsUsed = json.compilerOptionsUsed as node.CompilerOptions
171
+ const files = new Map(Object.entries<{ sourceCodeHash: string; warnings: string[] }>(json.infos))
172
+ return new ProjectArtifact(compilerOptionsUsed, files)
77
173
  }
174
+ }
78
175
 
79
- protected static async _handleImports(contractStr: string, importsCache: string[]): Promise<string> {
80
- const localImportsCache: string[] = []
81
- let result = contractStr.replace(Common.importRegex, (match) => {
82
- localImportsCache.push(match)
83
- return ''
176
+ export class Project {
177
+ sourceFiles: SourceFile[]
178
+ contracts: Compiled<Contract>[]
179
+ scripts: Compiled<Script>[]
180
+ projectArtifact: ProjectArtifact
181
+
182
+ readonly contractsRootPath: string
183
+ readonly artifactsRootPath: string
184
+ readonly nodeProvider: NodeProvider
185
+
186
+ static currentProject: Project
187
+
188
+ static readonly abstractContractMatcher = new TypedMatcher<SourceType>(
189
+ '^Abstract Contract [A-Z][a-zA-Z0-9]*',
190
+ SourceType.AbstractContract
191
+ )
192
+ static readonly contractMatcher = new TypedMatcher('^Contract [A-Z][a-zA-Z0-9]*', SourceType.Contract)
193
+ static readonly interfaceMatcher = new TypedMatcher('^Interface [A-Z][a-zA-Z0-9]* \\{', SourceType.Interface)
194
+ static readonly scriptMatcher = new TypedMatcher('^TxScript [A-Z][a-zA-Z0-9]*', SourceType.Script)
195
+ static readonly matchers = [
196
+ Project.abstractContractMatcher,
197
+ Project.contractMatcher,
198
+ Project.interfaceMatcher,
199
+ Project.scriptMatcher
200
+ ]
201
+
202
+ static buildProjectArtifact(
203
+ sourceFiles: SourceFile[],
204
+ contracts: Compiled<Contract>[],
205
+ scripts: Compiled<Script>[],
206
+ compilerOptions: node.CompilerOptions
207
+ ): ProjectArtifact {
208
+ const files: Map<string, { sourceCodeHash: string; warnings: string[] }> = new Map()
209
+ contracts.forEach((c) => {
210
+ files.set(c.sourceFile.contractPath, {
211
+ sourceCodeHash: c.sourceFile.sourceCodeHash,
212
+ warnings: c.warnings
213
+ })
214
+ })
215
+ scripts.forEach((s) => {
216
+ files.set(s.sourceFile.contractPath, {
217
+ sourceCodeHash: s.sourceFile.sourceCodeHash,
218
+ warnings: s.warnings
219
+ })
84
220
  })
85
- for (const myImport of localImportsCache) {
86
- const fileName = myImport.slice(8, -1)
87
- if (!importsCache.includes(fileName)) {
88
- importsCache.push(fileName)
89
- const importContractStr = await Common._loadContractStr(fileName, importsCache, (code) =>
90
- Contract.checkCodeType(fileName, code)
91
- )
92
- result = result.concat('\n', importContractStr)
221
+ const compiledSize = contracts.length + scripts.length
222
+ sourceFiles.slice(compiledSize).forEach((c) => {
223
+ files.set(c.contractPath, {
224
+ sourceCodeHash: c.sourceCodeHash,
225
+ warnings: []
226
+ })
227
+ })
228
+ return new ProjectArtifact(compilerOptions, files)
229
+ }
230
+
231
+ private constructor(
232
+ provider: NodeProvider,
233
+ contractsRootPath: string,
234
+ artifactsRootPath: string,
235
+ sourceFiles: SourceFile[],
236
+ contracts: Compiled<Contract>[],
237
+ scripts: Compiled<Script>[],
238
+ errorOnWarnings: boolean,
239
+ projectArtifact: ProjectArtifact
240
+ ) {
241
+ this.nodeProvider = provider
242
+ this.contractsRootPath = contractsRootPath
243
+ this.artifactsRootPath = artifactsRootPath
244
+ this.sourceFiles = sourceFiles
245
+ this.contracts = contracts
246
+ this.scripts = scripts
247
+ this.projectArtifact = projectArtifact
248
+
249
+ if (errorOnWarnings) {
250
+ Project.checkCompilerWarnings(
251
+ [...contracts.map((c) => c.warnings).flat(), ...scripts.map((s) => s.warnings).flat()],
252
+ errorOnWarnings
253
+ )
254
+ }
255
+ }
256
+
257
+ private getContractPath(path: string): string {
258
+ return path.startsWith(`./${this.contractsRootPath}`)
259
+ ? path.slice(2)
260
+ : path.startsWith(this.contractsRootPath)
261
+ ? path
262
+ : this.contractsRootPath + '/' + path
263
+ }
264
+
265
+ static checkCompilerWarnings(warnings: string[], errorOnWarnings: boolean): void {
266
+ if (warnings.length !== 0) {
267
+ const prefixPerWarning = ' - '
268
+ const warningString = prefixPerWarning + warnings.join('\n' + prefixPerWarning)
269
+ const output = `Compilation warnings:\n` + warningString + '\n'
270
+ if (errorOnWarnings) {
271
+ throw new Error(output)
272
+ } else {
273
+ console.log(output)
93
274
  }
94
275
  }
95
- return result
96
276
  }
97
277
 
98
- protected static async _loadContractStr(
99
- fileName: string,
100
- importsCache: string[],
101
- validate: (fileName: string) => void
102
- ): Promise<string> {
103
- const contractPath = this._contractPath(fileName)
104
- const contractBuffer = await fsPromises.readFile(contractPath)
105
- const contractStr = contractBuffer.toString()
278
+ static contract(path: string): Contract {
279
+ const contractPath = Project.currentProject.getContractPath(path)
280
+ const contract = Project.currentProject.contracts.find((c) => c.sourceFile.contractPath === contractPath)
281
+ if (typeof contract === 'undefined') {
282
+ throw new Error(`Contract ${contractPath} does not exist`)
283
+ }
284
+ return contract.artifact
285
+ }
286
+
287
+ static script(path: string): Script {
288
+ const contractPath = Project.currentProject.getContractPath(path)
289
+ const script = Project.currentProject.scripts.find((c) => c.sourceFile.contractPath === contractPath)
290
+ if (typeof script === 'undefined') {
291
+ throw new Error(`Script ${contractPath} does not exist`)
292
+ }
293
+ return script.artifact
294
+ }
106
295
 
107
- validate(contractStr)
108
- return Common._handleImports(contractStr, importsCache)
296
+ private async saveArtifactsToFile(): Promise<void> {
297
+ const artifactsRootPath = this.artifactsRootPath
298
+ const saveToFile = async function (compiled: Compiled<Artifact>): Promise<void> {
299
+ const artifactPath = compiled.sourceFile.getArtifactPath(artifactsRootPath)
300
+ const folder = artifactPath.slice(0, artifactPath.lastIndexOf('/'))
301
+ if (!fs.existsSync(folder)) {
302
+ fs.mkdirSync(folder, { recursive: true })
303
+ }
304
+ return fsPromises.writeFile(artifactPath, compiled.artifact.toString())
305
+ }
306
+ for (const contract of this.contracts) {
307
+ await saveToFile(contract)
308
+ }
309
+ for (const script of this.scripts) {
310
+ await saveToFile(script)
311
+ }
312
+ await this.projectArtifact.saveToFile(this.artifactsRootPath)
109
313
  }
110
314
 
111
- static checkFileNameExtension(fileName: string): void {
112
- if (!fileName.endsWith('.ral')) {
113
- throw new Error('Smart contract file name should end with ".ral"')
315
+ contractByCodeHash(codeHash: string): Contract {
316
+ const contract = this.contracts.find((c) => c.artifact.codeHash === codeHash)
317
+ if (typeof contract === 'undefined') {
318
+ throw new Error(`Unknown code with code hash: ${codeHash}`)
114
319
  }
320
+ return contract.artifact
115
321
  }
116
322
 
117
- protected static async _from<T extends { sourceCodeSha256: string }>(
323
+ private static async compile(
118
324
  provider: NodeProvider,
119
- fileName: string,
120
- loadContractStr: (fileName: string, importsCache: string[]) => Promise<string>,
121
- compile: (provider: NodeProvider, fileName: string, contractStr: string, contractHash: string) => Promise<T>
122
- ): Promise<T> {
123
- Common.checkFileNameExtension(fileName)
124
-
125
- const contractStr = await loadContractStr(fileName, [])
126
- const contractHash = cryptojs.SHA256(contractStr).toString()
127
- const existingContract = this._getArtifactFromCache(contractHash)
128
- if (typeof existingContract !== 'undefined') {
129
- return existingContract as unknown as T
325
+ files: SourceFile[],
326
+ contractsRootPath: string,
327
+ artifactsRootPath: string,
328
+ errorOnWarnings: boolean,
329
+ compilerOptions: node.CompilerOptions
330
+ ): Promise<Project> {
331
+ const sourceStr = files.map((f) => f.sourceCode).join('\n')
332
+ const result = await provider.contracts.postContractsCompileProject({
333
+ code: sourceStr,
334
+ compilerOptions: compilerOptions
335
+ })
336
+ const contracts: Compiled<Contract>[] = []
337
+ const scripts: Compiled<Script>[] = []
338
+ result.contracts.forEach((contractResult, index) => {
339
+ const sourceFile = files[`${index}`]
340
+ const contract = Contract.fromCompileResult(contractResult)
341
+ contracts.push(new Compiled(sourceFile, contract, contractResult.warnings))
342
+ })
343
+ result.scripts.forEach((scriptResult, index) => {
344
+ const sourceFile = files[index + contracts.length]
345
+ const script = Script.fromCompileResult(scriptResult)
346
+ scripts.push(new Compiled(sourceFile, script, scriptResult.warnings))
347
+ })
348
+ const projectArtifact = Project.buildProjectArtifact(files, contracts, scripts, compilerOptions)
349
+ const project = new Project(
350
+ provider,
351
+ contractsRootPath,
352
+ artifactsRootPath,
353
+ files,
354
+ contracts,
355
+ scripts,
356
+ errorOnWarnings,
357
+ projectArtifact
358
+ )
359
+ await project.saveArtifactsToFile()
360
+ return project
361
+ }
362
+
363
+ private static async loadArtifacts(
364
+ provider: NodeProvider,
365
+ files: SourceFile[],
366
+ projectArtifact: ProjectArtifact,
367
+ contractsRootPath: string,
368
+ artifactsRootPath: string,
369
+ errorOnWarnings: boolean,
370
+ compilerOptions: node.CompilerOptions
371
+ ): Promise<Project> {
372
+ try {
373
+ const contracts: Compiled<Contract>[] = []
374
+ const scripts: Compiled<Script>[] = []
375
+ for (const file of files) {
376
+ const info = projectArtifact.infos.get(file.contractPath)
377
+ if (typeof info === 'undefined') {
378
+ throw Error(`Unable to find project info for ${file.contractPath}, please rebuild the project`)
379
+ }
380
+ const warnings = info.warnings
381
+ const artifactPath = file.getArtifactPath(artifactsRootPath)
382
+ if (file.type === SourceType.Contract) {
383
+ const artifact = await Contract.fromArtifactFile(artifactPath)
384
+ contracts.push(new Compiled(file, artifact, warnings))
385
+ } else if (file.type === SourceType.Script) {
386
+ const artifact = await Script.fromArtifactFile(artifactPath)
387
+ scripts.push(new Compiled(file, artifact, warnings))
388
+ }
389
+ }
390
+
391
+ return new Project(
392
+ provider,
393
+ contractsRootPath,
394
+ artifactsRootPath,
395
+ files,
396
+ contracts,
397
+ scripts,
398
+ errorOnWarnings,
399
+ projectArtifact
400
+ )
401
+ } catch (error) {
402
+ console.log(`Failed to load artifacts, error: ${error}, try to re-compile contracts...`)
403
+ return Project.compile(provider, files, contractsRootPath, artifactsRootPath, errorOnWarnings, compilerOptions)
404
+ }
405
+ }
406
+
407
+ private static async loadSourceFile(dirPath: string, filename: string): Promise<SourceFile> {
408
+ const contractPath = dirPath + '/' + filename
409
+ if (!filename.endsWith('.ral')) {
410
+ throw new Error(`Invalid filename: ${contractPath}, smart contract file name should end with ".ral"`)
411
+ }
412
+
413
+ const sourceBuffer = await fsPromises.readFile(contractPath)
414
+ const sourceStr = sourceBuffer.toString()
415
+ const results = this.matchers.map((m) => m.match(sourceStr))
416
+ const matchNumber = results.reduce((a, b) => a + b, 0)
417
+ if (matchNumber === 0) {
418
+ throw new Error(`No contract defined in file: ${contractPath}`)
419
+ }
420
+ if (matchNumber > 1) {
421
+ throw new Error(`Multiple definitions in file: ${contractPath}`)
422
+ }
423
+ const matcherIndex = results.indexOf(1)
424
+ const type = this.matchers[`${matcherIndex}`].type
425
+ return SourceFile.from(type, sourceStr, contractPath)
426
+ }
427
+
428
+ private static async loadSourceFiles(contractsRootPath: string): Promise<SourceFile[]> {
429
+ const loadDir = async function (dirPath: string, results: SourceFile[]): Promise<void> {
430
+ const dirents = await fsPromises.readdir(dirPath, { withFileTypes: true })
431
+ for (const dirent of dirents) {
432
+ if (dirent.isFile()) {
433
+ const file = await Project.loadSourceFile(dirPath, dirent.name)
434
+ results.push(file)
435
+ } else {
436
+ const newPath = dirPath + '/' + dirent.name
437
+ await loadDir(newPath, results)
438
+ }
439
+ }
440
+ }
441
+ const sourceFiles: SourceFile[] = []
442
+ await loadDir(contractsRootPath, sourceFiles)
443
+ const contractAndScriptSize = sourceFiles.filter(
444
+ (f) => f.type === SourceType.Contract || f.type === SourceType.Script
445
+ ).length
446
+ if (sourceFiles.length === 0 || contractAndScriptSize === 0) {
447
+ throw new Error('Project have no source files')
448
+ }
449
+ return sourceFiles.sort((a, b) => a.type - b.type)
450
+ }
451
+
452
+ static async build(
453
+ compilerOptionsPartial: Partial<CompilerOptions> = {},
454
+ contractsRootPath = 'contracts',
455
+ artifactsRootPath = 'artifacts'
456
+ ): Promise<void> {
457
+ const provider = getCurrentNodeProvider()
458
+ const sourceFiles = await Project.loadSourceFiles(contractsRootPath)
459
+ const { errorOnWarnings, ...nodeCompilerOptions } = { ...DEFAULT_COMPILER_OPTIONS, ...compilerOptionsPartial }
460
+ const projectArtifact = await ProjectArtifact.from(artifactsRootPath)
461
+ if (typeof projectArtifact === 'undefined' || projectArtifact.needToReCompile(nodeCompilerOptions, sourceFiles)) {
462
+ console.log(`Compile contracts in folder "${contractsRootPath}"`)
463
+ Project.currentProject = await Project.compile(
464
+ provider,
465
+ sourceFiles,
466
+ contractsRootPath,
467
+ artifactsRootPath,
468
+ errorOnWarnings,
469
+ nodeCompilerOptions
470
+ )
130
471
  } else {
131
- return compile(provider, fileName, contractStr, contractHash)
472
+ console.log(`Load compiled contracts from folder "${artifactsRootPath}"`)
473
+ Project.currentProject = await Project.loadArtifacts(
474
+ provider,
475
+ sourceFiles,
476
+ projectArtifact,
477
+ contractsRootPath,
478
+ artifactsRootPath,
479
+ errorOnWarnings,
480
+ nodeCompilerOptions
481
+ )
132
482
  }
133
483
  }
484
+ }
134
485
 
135
- protected _saveToFile(fileName: string): Promise<void> {
136
- const artifactPath = Common._artifactPath(fileName)
137
- return fsPromises.writeFile(artifactPath, this.toString())
486
+ export abstract class Artifact {
487
+ readonly functions: FunctionSig[]
488
+
489
+ constructor(functions: FunctionSig[]) {
490
+ this.functions = functions
138
491
  }
139
492
 
140
493
  abstract buildByteCodeToDeploy(initialFields?: Fields): string
494
+
495
+ publicFunctions(): string[] {
496
+ return this.functions.filter((func) => func.isPublic).map((func) => func.name)
497
+ }
498
+
499
+ usingPreapprovedAssetsFunctions(): string[] {
500
+ return this.functions.filter((func) => func.usePreapprovedAssets).map((func) => func.name)
501
+ }
502
+
503
+ usingAssetsInContractFunctions(): string[] {
504
+ return this.functions.filter((func) => func.useAssetsInContract).map((func) => func.name)
505
+ }
141
506
  }
142
507
 
143
- export class Contract extends Common {
508
+ export class Contract extends Artifact {
144
509
  readonly bytecode: string
145
510
  readonly codeHash: string
146
- readonly fieldsSig: node.FieldsSig
147
- readonly eventsSig: node.EventSig[]
511
+ readonly fieldsSig: FieldsSig
512
+ readonly eventsSig: EventSig[]
148
513
 
149
514
  constructor(
150
- sourceCodeSha256: string,
151
515
  bytecode: string,
152
516
  codeHash: string,
153
- fieldsSig: node.FieldsSig,
154
- eventsSig: node.EventSig[],
155
- functions: node.FunctionSig[]
517
+ fieldsSig: FieldsSig,
518
+ eventsSig: EventSig[],
519
+ functions: FunctionSig[]
156
520
  ) {
157
- super(sourceCodeSha256, functions)
521
+ super(functions)
158
522
  this.bytecode = bytecode
159
523
  this.codeHash = codeHash
160
524
  this.fieldsSig = fieldsSig
161
525
  this.eventsSig = eventsSig
162
526
  }
163
527
 
164
- static checkCodeType(fileName: string, contractStr: string): void {
165
- const interfaceMatches = contractStr.match(Contract.interfaceRegex)
166
- if (interfaceMatches) {
167
- return
168
- }
169
-
170
- const contractMatches = contractStr.match(Contract.contractRegex)
171
- if (contractMatches === null) {
172
- throw new Error(`No contract found in: ${fileName}`)
173
- } else if (contractMatches.length > 1) {
174
- throw new Error(`Multiple contracts in: ${fileName}`)
175
- } else {
176
- return
177
- }
178
- }
179
-
180
- private static async loadContractStr(fileName: string, importsCache: string[]): Promise<string> {
181
- return Common._loadContractStr(fileName, importsCache, (code) => Contract.checkCodeType(fileName, code))
182
- }
183
-
184
- static async fromSource(provider: NodeProvider, fileName: string): Promise<Contract> {
185
- if (!fs.existsSync(Common._artifactsFolder())) {
186
- fs.mkdirSync(Common._artifactsFolder(), { recursive: true })
187
- }
188
- const contract = await Common._from(
189
- provider,
190
- fileName,
191
- (fileName, importCaches) => Contract.loadContractStr(fileName, importCaches),
192
- Contract.compile
193
- )
194
- this._putArtifactToCache(contract)
195
- return contract
196
- }
197
-
198
- private static async compile(
199
- provider: NodeProvider,
200
- fileName: string,
201
- contractStr: string,
202
- contractHash: string
203
- ): Promise<Contract> {
204
- const compiled = await provider.contracts.postContractsCompileContract({ code: contractStr })
205
- const artifact = new Contract(
206
- contractHash,
207
- compiled.bytecode,
208
- compiled.codeHash,
209
- compiled.fields,
210
- compiled.events,
211
- compiled.functions
212
- )
213
- await artifact._saveToFile(fileName)
214
- return artifact
215
- }
216
-
217
528
  // TODO: safely parse json
218
529
  static fromJson(artifact: any): Contract {
219
530
  if (
220
- artifact.sourceCodeSha256 == null ||
221
531
  artifact.bytecode == null ||
222
532
  artifact.codeHash == null ||
223
533
  artifact.fieldsSig == null ||
@@ -227,38 +537,42 @@ export class Contract extends Common {
227
537
  throw Error('The artifact JSON for contract is incomplete')
228
538
  }
229
539
  const contract = new Contract(
230
- artifact.sourceCodeSha256,
231
540
  artifact.bytecode,
232
541
  artifact.codeHash,
233
542
  artifact.fieldsSig,
234
543
  artifact.eventsSig,
235
544
  artifact.functions
236
545
  )
237
- this._putArtifactToCache(contract)
238
546
  return contract
239
547
  }
240
548
 
549
+ static fromCompileResult(result: CompileContractResult): Contract {
550
+ return new Contract(result.bytecode, result.codeHash, result.fields, result.events, result.functions)
551
+ }
552
+
241
553
  // support both 'code.ral' and 'code.ral.json'
242
- static async fromArtifactFile(fileName: string): Promise<Contract> {
243
- const artifactPath = Contract._artifactPath(fileName)
244
- const content = await fsPromises.readFile(artifactPath)
554
+ static async fromArtifactFile(path: string): Promise<Contract> {
555
+ const content = await fsPromises.readFile(path)
245
556
  const artifact = JSON.parse(content.toString())
246
557
  return Contract.fromJson(artifact)
247
558
  }
248
559
 
560
+ async fetchState(address: string, group: number): Promise<ContractState> {
561
+ const state = await Project.currentProject.nodeProvider.contracts.getContractsAddressState(address, {
562
+ group: group
563
+ })
564
+ return this.fromApiContractState(state)
565
+ }
566
+
249
567
  override toString(): string {
250
- return JSON.stringify(
251
- {
252
- sourceCodeSha256: this.sourceCodeSha256,
253
- bytecode: this.bytecode,
254
- codeHash: this.codeHash,
255
- fieldsSig: this.fieldsSig,
256
- eventsSig: this.eventsSig,
257
- functions: this.functions
258
- },
259
- null,
260
- 2
261
- )
568
+ const object = {
569
+ bytecode: this.bytecode,
570
+ codeHash: this.codeHash,
571
+ fieldsSig: this.fieldsSig,
572
+ eventsSig: this.eventsSig,
573
+ functions: this.functions
574
+ }
575
+ return JSON.stringify(object, null, 2)
262
576
  }
263
577
 
264
578
  toState(fields: Fields, asset: Asset, address?: string): ContractState {
@@ -274,25 +588,26 @@ export class Contract extends Common {
274
588
  }
275
589
  }
276
590
 
591
+ // no need to be cryptographically strong random
277
592
  static randomAddress(): string {
278
- const bytes = crypto.randomBytes(33)
593
+ const bytes = new Uint8Array(33)
594
+ crypto.getRandomValues(bytes)
279
595
  bytes[0] = 3
280
596
  return bs58.encode(bytes)
281
597
  }
282
598
 
283
599
  private async _test(
284
- provider: NodeProvider,
285
600
  funcName: string,
286
601
  params: TestContractParams,
287
602
  expectPublic: boolean,
288
603
  accessType: string
289
604
  ): Promise<TestContractResult> {
290
605
  const apiParams: node.TestContract = this.toTestContract(funcName, params)
291
- const apiResult = await provider.contracts.postContractsTestContract(apiParams)
606
+ const apiResult = await Project.currentProject.nodeProvider.contracts.postContractsTestContract(apiParams)
292
607
 
293
608
  const methodIndex =
294
609
  typeof params.testMethodIndex !== 'undefined' ? params.testMethodIndex : this.getMethodIndex(funcName)
295
- const isPublic = this.functions[`${methodIndex}`].signature.indexOf('pub ') !== -1
610
+ const isPublic = this.functions[`${methodIndex}`].isPublic
296
611
  if (isPublic === expectPublic) {
297
612
  const result = await this.fromTestContractResult(methodIndex, apiResult)
298
613
  return result
@@ -301,20 +616,12 @@ export class Contract extends Common {
301
616
  }
302
617
  }
303
618
 
304
- async testPublicMethod(
305
- provider: NodeProvider,
306
- funcName: string,
307
- params: TestContractParams
308
- ): Promise<TestContractResult> {
309
- return this._test(provider, funcName, params, true, 'public')
619
+ async testPublicMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
620
+ return this._test(funcName, params, true, 'public')
310
621
  }
311
622
 
312
- async testPrivateMethod(
313
- provider: NodeProvider,
314
- funcName: string,
315
- params: TestContractParams
316
- ): Promise<TestContractResult> {
317
- return this._test(provider, funcName, params, false, 'private')
623
+ async testPrivateMethod(funcName: string, params: TestContractParams): Promise<TestContractResult> {
624
+ return this._test(funcName, params, false, 'private')
318
625
  }
319
626
 
320
627
  toApiFields(fields?: Fields): node.Val[] {
@@ -360,33 +667,8 @@ export class Contract extends Common {
360
667
  }
361
668
  }
362
669
 
363
- static async fromCodeHash(codeHash: string): Promise<Contract> {
364
- const cached = this._getArtifactFromCache(codeHash)
365
- if (typeof cached !== 'undefined') {
366
- return cached as Contract
367
- }
368
-
369
- const files = await fsPromises.readdir(Common._artifactsFolder())
370
- for (const file of files) {
371
- if (file.endsWith('.ral.json')) {
372
- try {
373
- const contract = await Contract.fromArtifactFile(file)
374
- if (contract.codeHash === codeHash) {
375
- return contract as Contract
376
- }
377
- } catch (_) {}
378
- }
379
- }
380
-
381
- throw new Error(`Unknown code with code hash: ${codeHash}`)
382
- }
383
-
384
- static async getFieldsSig(state: node.ContractState): Promise<node.FieldsSig> {
385
- return Contract.fromCodeHash(state.codeHash).then((contract) => contract.fieldsSig)
386
- }
387
-
388
670
  async fromApiContractState(state: node.ContractState): Promise<ContractState> {
389
- const contract = await Contract.fromCodeHash(state.codeHash)
671
+ const contract = Project.currentProject.contractByCodeHash(state.codeHash)
390
672
  return {
391
673
  address: state.address,
392
674
  contractId: binToHex(contractIdFromAddress(state.address)),
@@ -394,21 +676,19 @@ export class Contract extends Common {
394
676
  initialStateHash: state.initialStateHash,
395
677
  codeHash: state.codeHash,
396
678
  fields: fromApiFields(state.fields, contract.fieldsSig),
397
- fieldsSig: await Contract.getFieldsSig(state),
679
+ fieldsSig: contract.fieldsSig,
398
680
  asset: fromApiAsset(state.asset)
399
681
  }
400
682
  }
401
683
 
402
- static ContractCreatedEvent: node.EventSig = {
684
+ static ContractCreatedEvent: EventSig = {
403
685
  name: 'ContractCreated',
404
- signature: 'event ContractCreated(address:Address)',
405
686
  fieldNames: ['address'],
406
687
  fieldTypes: ['Address']
407
688
  }
408
689
 
409
- static ContractDestroyedEvent: node.EventSig = {
690
+ static ContractDestroyedEvent: EventSig = {
410
691
  name: 'ContractDestroyed',
411
- signature: 'event ContractDestroyed(address:Address)',
412
692
  fieldNames: ['address'],
413
693
  fieldTypes: ['Address']
414
694
  }
@@ -417,14 +697,14 @@ export class Contract extends Common {
417
697
  event: node.ContractEventByTxId,
418
698
  codeHash: string | undefined
419
699
  ): Promise<ContractEventByTxId> {
420
- let eventSig: node.EventSig
700
+ let eventSig: EventSig
421
701
 
422
702
  if (event.eventIndex == -1) {
423
703
  eventSig = this.ContractCreatedEvent
424
704
  } else if (event.eventIndex == -2) {
425
705
  eventSig = this.ContractDestroyedEvent
426
706
  } else {
427
- const contract = await Contract.fromCodeHash(codeHash!)
707
+ const contract = Project.currentProject.contractByCodeHash(codeHash!)
428
708
  eventSig = contract.eventsSig[event.eventIndex]
429
709
  }
430
710
 
@@ -492,88 +772,41 @@ export class Contract extends Common {
492
772
  }
493
773
  }
494
774
 
495
- export class Script extends Common {
775
+ export class Script extends Artifact {
496
776
  readonly bytecodeTemplate: string
497
- readonly fieldsSig: node.FieldsSig
777
+ readonly fieldsSig: FieldsSig
498
778
 
499
- constructor(
500
- sourceCodeSha256: string,
501
- bytecodeTemplate: string,
502
- fieldsSig: node.FieldsSig,
503
- functions: node.FunctionSig[]
504
- ) {
505
- super(sourceCodeSha256, functions)
779
+ constructor(bytecodeTemplate: string, fieldsSig: FieldsSig, functions: FunctionSig[]) {
780
+ super(functions)
506
781
  this.bytecodeTemplate = bytecodeTemplate
507
782
  this.fieldsSig = fieldsSig
508
783
  }
509
784
 
510
- static checkCodeType(fileName: string, contractStr: string): void {
511
- const scriptMatches = contractStr.match(this.scriptRegex)
512
- if (scriptMatches === null) {
513
- throw new Error(`No script found in: ${fileName}`)
514
- } else if (scriptMatches.length > 1) {
515
- throw new Error(`Multiple scripts in: ${fileName}`)
516
- } else {
517
- return
518
- }
519
- }
520
-
521
- private static async loadContractStr(fileName: string, importsCache: string[]): Promise<string> {
522
- return Common._loadContractStr(fileName, importsCache, (code) => Script.checkCodeType(fileName, code))
523
- }
524
-
525
- static async fromSource(provider: NodeProvider, fileName: string): Promise<Script> {
526
- return Common._from(
527
- provider,
528
- fileName,
529
- (fileName, importsCache) => Script.loadContractStr(fileName, importsCache),
530
- Script.compile
531
- )
532
- }
533
-
534
- private static async compile(
535
- provider: NodeProvider,
536
- fileName: string,
537
- scriptStr: string,
538
- contractHash: string
539
- ): Promise<Script> {
540
- const compiled = await provider.contracts.postContractsCompileScript({ code: scriptStr })
541
- const artifact = new Script(contractHash, compiled.bytecodeTemplate, compiled.fields, compiled.functions)
542
- await artifact._saveToFile(fileName)
543
- return artifact
785
+ static fromCompileResult(result: CompileScriptResult): Script {
786
+ return new Script(result.bytecodeTemplate, result.fields, result.functions)
544
787
  }
545
788
 
546
789
  // TODO: safely parse json
547
790
  static fromJson(artifact: any): Script {
548
- if (
549
- artifact.sourceCodeSha256 == null ||
550
- artifact.bytecodeTemplate == null ||
551
- artifact.fieldsSig == null ||
552
- artifact.functions == null
553
- ) {
791
+ if (artifact.bytecodeTemplate == null || artifact.fieldsSig == null || artifact.functions == null) {
554
792
  throw Error('The artifact JSON for script is incomplete')
555
793
  }
556
- return new Script(artifact.sourceCodeSha256, artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
794
+ return new Script(artifact.bytecodeTemplate, artifact.fieldsSig, artifact.functions)
557
795
  }
558
796
 
559
- static async fromArtifactFile(fileName: string): Promise<Script> {
560
- const artifactPath = Common._artifactPath(fileName)
561
- const content = await fsPromises.readFile(artifactPath)
797
+ static async fromArtifactFile(path: string): Promise<Script> {
798
+ const content = await fsPromises.readFile(path)
562
799
  const artifact = JSON.parse(content.toString())
563
800
  return this.fromJson(artifact)
564
801
  }
565
802
 
566
803
  override toString(): string {
567
- return JSON.stringify(
568
- {
569
- sourceCodeSha256: this.sourceCodeSha256,
570
- bytecodeTemplate: this.bytecodeTemplate,
571
- fieldsSig: this.fieldsSig,
572
- functions: this.functions
573
- },
574
- null,
575
- 2
576
- )
804
+ const object = {
805
+ bytecodeTemplate: this.bytecodeTemplate,
806
+ fieldsSig: this.fieldsSig,
807
+ functions: this.functions
808
+ }
809
+ return JSON.stringify(object, null, 2)
577
810
  }
578
811
 
579
812
  async paramsForDeployment(params: BuildExecuteScriptTx): Promise<SignExecuteScriptTxParams> {
@@ -849,7 +1082,7 @@ export interface ContractState {
849
1082
  initialStateHash?: string
850
1083
  codeHash: string
851
1084
  fields: Fields
852
- fieldsSig: node.FieldsSig
1085
+ fieldsSig: FieldsSig
853
1086
  asset: Asset
854
1087
  }
855
1088
 
@@ -872,12 +1105,12 @@ function toApiContractState(state: ContractState): node.ContractState {
872
1105
  }
873
1106
  }
874
1107
 
875
- function toApiFields(fields: Fields, fieldsSig: node.FieldsSig): node.Val[] {
1108
+ function toApiFields(fields: Fields, fieldsSig: FieldsSig): node.Val[] {
876
1109
  return toApiVals(fields, fieldsSig.names, fieldsSig.types)
877
1110
  }
878
1111
 
879
- function toApiArgs(args: Arguments, funcSig: node.FunctionSig): node.Val[] {
880
- return toApiVals(args, funcSig.argNames, funcSig.argTypes)
1112
+ function toApiArgs(args: Arguments, funcSig: FunctionSig): node.Val[] {
1113
+ return toApiVals(args, funcSig.paramNames, funcSig.paramTypes)
881
1114
  }
882
1115
 
883
1116
  function toApiVals(fields: Fields, names: string[], types: string[]): node.Val[] {