@btc-vision/btc-runtime 1.10.12 → 1.11.0-alpha

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/docs/README.md CHANGED
@@ -285,6 +285,18 @@ npm run build:token
285
285
  - [Common Mistakes](./contracts/reentrancy-guard.md#common-mistakes)
286
286
  - [Testing Reentrancy](./contracts/reentrancy-guard.md#testing-reentrancy)
287
287
 
288
+ #### [Upgradeable](./contracts/upgradeable.md)
289
+ - [Overview](./contracts/upgradeable.md#overview)
290
+ - [Class Reference](./contracts/upgradeable.md#class-reference)
291
+ - [Properties](./contracts/upgradeable.md#properties)
292
+ - [Methods](./contracts/upgradeable.md#methods)
293
+ - [Events](./contracts/upgradeable.md#events)
294
+ - [Usage Patterns](./contracts/upgradeable.md#usage-patterns)
295
+ - [Security Considerations](./contracts/upgradeable.md#security-considerations)
296
+ - [Upgrade Workflow](./contracts/upgradeable.md#upgrade-workflow)
297
+ - [Combining with Other Base Classes](./contracts/upgradeable.md#combining-with-other-base-classes)
298
+ - [Solidity Comparison](./contracts/upgradeable.md#solidity-comparison)
299
+
288
300
  ---
289
301
 
290
302
  ### Types & Utilities
@@ -487,14 +499,30 @@ npm run build:token
487
499
  - [Solidity vs OPNet Comparison](./advanced/bitcoin-scripts.md#solidity-vs-opnet-comparison)
488
500
  - [Best Practices](./advanced/bitcoin-scripts.md#best-practices)
489
501
 
502
+ #### [Contract Upgrades](./advanced/contract-upgrades.md)
503
+ - [Overview](./advanced/contract-upgrades.md#overview)
504
+ - [How It Works](./advanced/contract-upgrades.md#how-it-works)
505
+ - [Basic Usage](./advanced/contract-upgrades.md#basic-usage)
506
+ - [The Timelock Pattern](./advanced/contract-upgrades.md#the-timelock-pattern)
507
+ - [Why Use a Timelock?](./advanced/contract-upgrades.md#why-use-a-timelock)
508
+ - [Using the Upgradeable Base Class](./advanced/contract-upgrades.md#using-the-upgradeable-base-class)
509
+ - [Using the UpgradeablePlugin](./advanced/contract-upgrades.md#using-the-upgradeableplugin)
510
+ - [Storage Compatibility](./advanced/contract-upgrades.md#storage-compatibility)
511
+ - [Security Considerations](./advanced/contract-upgrades.md#security-considerations)
512
+ - [Comparison with Other Platforms](./advanced/contract-upgrades.md#comparison-with-other-platforms)
513
+ - [Complete Example](./advanced/contract-upgrades.md#complete-example)
514
+ - [Upgrade Workflow](./advanced/contract-upgrades.md#upgrade-workflow)
515
+
490
516
  #### [Plugins](./advanced/plugins.md)
491
517
  - [Overview](./advanced/plugins.md#overview)
492
518
  - [Plugin Architecture](./advanced/plugins.md#plugin-architecture)
493
519
  - [Creating Plugins](./advanced/plugins.md#creating-plugins)
494
520
  - [Lifecycle Hooks](./advanced/plugins.md#lifecycle-hooks)
495
521
  - [onDeployment](./advanced/plugins.md#ondeployment)
522
+ - [onUpdate](./advanced/plugins.md#onupdate)
496
523
  - [onExecutionStarted](./advanced/plugins.md#onexecutionstarted)
497
524
  - [onExecutionCompleted](./advanced/plugins.md#onexecutioncompleted)
525
+ - [execute](./advanced/plugins.md#execute)
498
526
  - [Built-in Plugins](./advanced/plugins.md#built-in-plugins)
499
527
  - [Solidity vs OPNet Comparison](./advanced/plugins.md#solidity-vs-opnet-comparison)
500
528
  - [Best Practices](./advanced/plugins.md#best-practices)
@@ -0,0 +1,537 @@
1
+ # Contract Upgrades
2
+
3
+ OPNet provides a native bytecode replacement mechanism that allows contracts to upgrade their execution logic while preserving their address and storage state. This guide covers the upgrade mechanism, security considerations, and the timelock pattern for safe upgrades.
4
+
5
+ ## Overview
6
+
7
+ Unlike Ethereum's proxy patterns or Solana's upgrade authority model, OPNet enables contracts to replace their own bytecode through a VM opcode. The mechanism uses an address-based replacement model where new bytecode is deployed to a temporary contract, and the target contract references that address to perform the upgrade.
8
+
9
+ ```typescript
10
+ import { Blockchain } from '@btc-vision/btc-runtime/runtime';
11
+
12
+ // Deploy new bytecode as a separate contract first
13
+ // Then update the current contract's bytecode
14
+ Blockchain.updateContractFromExisting(newBytecodeAddress);
15
+ // New bytecode takes effect at the next block
16
+ ```
17
+
18
+ ## How It Works
19
+
20
+ ```mermaid
21
+ sequenceDiagram
22
+ participant Owner as Contract Owner
23
+ participant Target as Target Contract
24
+ participant Source as Source Contract<br/>(New Bytecode)
25
+ participant VM as OPNet VM
26
+
27
+ Note over Owner: Step 1: Deploy new bytecode
28
+ Owner->>VM: Deploy new contract with updated code
29
+ VM->>Source: Create contract at new address
30
+
31
+ Note over Owner: Step 2: Call upgrade function
32
+ Owner->>Target: upgrade(sourceAddress)
33
+ Target->>Target: Validate permissions
34
+ Target->>Target: Validate source is a contract
35
+
36
+ Note over Target,VM: Step 3: Execute bytecode replacement
37
+ Target->>VM: updateContractFromExisting(sourceAddress)
38
+ VM->>Source: Read bytecode
39
+ VM->>VM: Schedule replacement for next block
40
+
41
+ Note over VM: Block boundary
42
+ VM->>Target: Replace bytecode
43
+
44
+ Note over Target: Step 4: New code active
45
+ Owner->>Target: Any call now executes new bytecode
46
+ ```
47
+
48
+ ## Basic Usage
49
+
50
+ ### The updateContractFromExisting Method
51
+
52
+ ```typescript
53
+ /**
54
+ * Updates this contract's bytecode from an existing deployed contract.
55
+ * New bytecode takes effect at the next block.
56
+ *
57
+ * @param sourceAddress - Address of the contract containing the new bytecode
58
+ * @param calldata - Optional calldata passed to the VM (default: empty)
59
+ */
60
+ Blockchain.updateContractFromExisting(
61
+ sourceAddress: Address,
62
+ calldata?: BytesWriter | null
63
+ ): void
64
+ ```
65
+
66
+ ### Simple Upgrade Function
67
+
68
+ ```typescript
69
+ import {
70
+ OP_NET,
71
+ Blockchain,
72
+ Address,
73
+ Calldata,
74
+ BytesWriter,
75
+ encodeSelector,
76
+ Revert,
77
+ } from '@btc-vision/btc-runtime/runtime';
78
+
79
+ @final
80
+ export class MyContract extends OP_NET {
81
+ public override execute(method: Selector, calldata: Calldata): BytesWriter {
82
+ switch (method) {
83
+ case encodeSelector('upgrade(address)'):
84
+ return this.upgrade(calldata);
85
+ default:
86
+ return super.execute(method, calldata);
87
+ }
88
+ }
89
+
90
+ private upgrade(calldata: Calldata): BytesWriter {
91
+ // Only deployer can upgrade
92
+ this.onlyDeployer(Blockchain.tx.sender);
93
+
94
+ const sourceAddress = calldata.readAddress();
95
+
96
+ // Validate source is a deployed contract
97
+ if (!Blockchain.isContract(sourceAddress)) {
98
+ throw new Revert('Source must be a deployed contract');
99
+ }
100
+
101
+ // Perform upgrade - takes effect next block
102
+ Blockchain.updateContractFromExisting(sourceAddress);
103
+
104
+ return new BytesWriter(0);
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## The Timelock Pattern
110
+
111
+ Immediate upgrades are risky because users have no time to react to potentially malicious changes. The timelock pattern addresses this by requiring a delay between submitting and applying an upgrade.
112
+
113
+ ### Why Use a Timelock?
114
+
115
+ 1. **User Protection**: Users can monitor for pending upgrades and exit if they distrust changes
116
+ 2. **Attack Prevention**: Prevents instant malicious upgrades
117
+ 3. **Transparency**: All pending upgrades are visible on-chain
118
+
119
+ ### Timelock Flow
120
+
121
+ ```mermaid
122
+ sequenceDiagram
123
+ participant Owner as Contract Owner
124
+ participant Contract as Upgradeable Contract
125
+ participant Users as Users/Indexers
126
+
127
+ Note over Owner: Day 1: Submit upgrade
128
+ Owner->>Contract: submitUpgrade(sourceAddress)
129
+ Contract->>Contract: Store pending address + block
130
+ Contract-->>Users: Emit UpgradeSubmitted event
131
+
132
+ Note over Users: Users can monitor and exit
133
+ Users->>Users: Review pending upgrade
134
+ Users->>Users: Exit if distrustful
135
+
136
+ Note over Owner: Day 2+: Apply after delay
137
+ Owner->>Contract: applyUpgrade(sourceAddress)
138
+ Contract->>Contract: Verify delay elapsed
139
+ Contract->>Contract: Verify address matches
140
+ Contract->>Contract: Execute upgrade
141
+ Contract-->>Users: Emit UpgradeApplied event
142
+ ```
143
+
144
+ ### Using the Upgradeable Base Class
145
+
146
+ ```typescript
147
+ import {
148
+ Upgradeable,
149
+ Calldata,
150
+ BytesWriter,
151
+ encodeSelector,
152
+ Selector,
153
+ ADDRESS_BYTE_LENGTH,
154
+ } from '@btc-vision/btc-runtime/runtime';
155
+
156
+ @final
157
+ export class MyUpgradeableContract extends Upgradeable {
158
+ // Set upgrade delay: 144 blocks = ~24 hours
159
+ protected readonly upgradeDelay: u64 = 144;
160
+
161
+ public override execute(method: Selector, calldata: Calldata): BytesWriter {
162
+ switch (method) {
163
+ case encodeSelector('submitUpgrade'):
164
+ return this.submitUpgrade(calldata.readAddress());
165
+ case encodeSelector('applyUpgrade'): {
166
+ const sourceAddress = calldata.readAddress();
167
+ const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
168
+ const updateCalldata = new BytesWriter(remainingLength);
169
+ if (remainingLength > 0) {
170
+ updateCalldata.writeBytes(calldata.readBytes(remainingLength));
171
+ }
172
+ return this.applyUpgrade(sourceAddress, updateCalldata);
173
+ }
174
+ case encodeSelector('cancelUpgrade'):
175
+ return this.cancelUpgrade();
176
+ // ... other methods
177
+ default:
178
+ return super.execute(method, calldata);
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### Using the UpgradeablePlugin
185
+
186
+ If you don't want to extend a base class (for example, if you're already extending `OP20` or `OP721`), use the `UpgradeablePlugin` instead. The plugin system is fully automatic - just register the plugin in your constructor:
187
+
188
+ ```typescript
189
+ import {
190
+ OP20,
191
+ UpgradeablePlugin,
192
+ } from '@btc-vision/btc-runtime/runtime';
193
+
194
+ @final
195
+ export class MyToken extends OP20 {
196
+ public constructor() {
197
+ super();
198
+ // Register the plugin - 144 blocks = ~24 hours (default)
199
+ this.registerPlugin(new UpgradeablePlugin(144));
200
+ }
201
+
202
+ // No need to modify execute() - upgrade methods are handled automatically!
203
+ }
204
+ ```
205
+
206
+ The plugin automatically handles these methods:
207
+ - `submitUpgrade(address)` - Submit upgrade for timelock
208
+ - `applyUpgrade(address)` - Apply upgrade after delay
209
+ - `cancelUpgrade()` - Cancel pending upgrade
210
+ - `pendingUpgrade()` - Get pending upgrade info
211
+ - `upgradeDelay()` - Get configured delay
212
+
213
+ **How it works:** When your contract's `execute()` falls through to `super.execute()`, the base `OP_NET` class automatically checks all registered plugins before throwing "Method not found".
214
+
215
+ ### Common Delay Values
216
+
217
+ | Delay | Blocks | Use Case |
218
+ |-------|--------|----------|
219
+ | ~1 hour | 6 | Emergency patches (not recommended for production) |
220
+ | ~24 hours | 144 | Standard upgrades (default) |
221
+ | ~1 week | 1008 | Critical infrastructure |
222
+ | ~1 month | 4320 | Governance-controlled contracts |
223
+
224
+ ### Upgrade Events
225
+
226
+ The `Upgradeable` contract emits events for monitoring:
227
+
228
+ ```typescript
229
+ // Emitted when an upgrade is submitted
230
+ class UpgradeSubmittedEvent {
231
+ sourceAddress: Address; // New bytecode contract
232
+ submitBlock: u64; // Block when submitted
233
+ effectiveBlock: u64; // Block when upgrade can be applied
234
+ }
235
+
236
+ // Emitted when an upgrade is applied
237
+ class UpgradeAppliedEvent {
238
+ sourceAddress: Address; // New bytecode contract
239
+ appliedAtBlock: u64; // Block when applied
240
+ }
241
+
242
+ // Emitted when a pending upgrade is cancelled
243
+ class UpgradeCancelledEvent {
244
+ sourceAddress: Address; // Cancelled source contract
245
+ cancelledAtBlock: u64; // Block when cancelled
246
+ }
247
+ ```
248
+
249
+ ## Storage Compatibility
250
+
251
+ Storage layout compatibility across bytecode versions is the developer's responsibility. The VM does not validate or migrate storage between versions.
252
+
253
+ ### What Persists
254
+
255
+ - Contract address (unchanged)
256
+ - All storage slots (unchanged)
257
+ - Contract deployer
258
+
259
+ ### What Changes
260
+
261
+ - Execution logic (bytecode)
262
+
263
+ ### Storage Layout Rules
264
+
265
+ ```typescript
266
+ // Version 1
267
+ class MyContractV1 extends OP_NET {
268
+ private balancePointer: u16 = Blockchain.nextPointer; // Pointer 1
269
+ private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 2
270
+ }
271
+
272
+ // Version 2 - CORRECT: Add new pointers at the end
273
+ class MyContractV2 extends OP_NET {
274
+ private balancePointer: u16 = Blockchain.nextPointer; // Pointer 1 (same)
275
+ private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 2 (same)
276
+ private newFeaturePointer: u16 = Blockchain.nextPointer; // Pointer 3 (new)
277
+ }
278
+
279
+ // Version 2 - WRONG: Changing order breaks storage
280
+ class MyContractV2Bad extends OP_NET {
281
+ private newFeaturePointer: u16 = Blockchain.nextPointer; // Pointer 1 (was balance!)
282
+ private balancePointer: u16 = Blockchain.nextPointer; // Pointer 2 (was allowance!)
283
+ private allowancePointer: u16 = Blockchain.nextPointer; // Pointer 3 (new slot)
284
+ }
285
+ ```
286
+
287
+ ### Best Practices
288
+
289
+ 1. **Never remove or reorder existing pointers**
290
+ 2. **Always add new pointers at the end**
291
+ 3. **Document pointer assignments**
292
+ 4. **Test upgrades thoroughly on testnet**
293
+
294
+ ## The onUpdate Lifecycle Hook
295
+
296
+ When a contract's bytecode is updated via `updateContractFromExisting`, the VM calls the `onUpdate` hook on the **new** bytecode. This allows the new contract version to perform migrations, initialize new storage, or validate the upgrade.
297
+
298
+ ### Basic Usage
299
+
300
+ ```typescript
301
+ @final
302
+ export class MyContractV2 extends OP_NET {
303
+ // New storage pointer added in V2
304
+ private newFeaturePointer: u16 = Blockchain.nextPointer;
305
+ private _newFeature: StoredU256;
306
+
307
+ public constructor() {
308
+ super();
309
+ this._newFeature = new StoredU256(this.newFeaturePointer, EMPTY_POINTER);
310
+ }
311
+
312
+ public override onUpdate(calldata: Calldata): void {
313
+ super.onUpdate(calldata); // Call plugins first
314
+
315
+ // Initialize new storage with default value
316
+ if (this._newFeature.value === u256.Zero) {
317
+ this._newFeature.value = u256.fromU64(100);
318
+ }
319
+ }
320
+ }
321
+ ```
322
+
323
+ ### Version-Based Migrations
324
+
325
+ For complex migrations, pass a version number in the calldata:
326
+
327
+ ```typescript
328
+ public override onUpdate(calldata: Calldata): void {
329
+ super.onUpdate(calldata);
330
+
331
+ // Read migration version from calldata
332
+ const fromVersion = calldata.readU64();
333
+
334
+ if (fromVersion === 1) {
335
+ this.migrateFromV1();
336
+ } else if (fromVersion === 2) {
337
+ this.migrateFromV2();
338
+ }
339
+ }
340
+
341
+ private migrateFromV1(): void {
342
+ // Migration logic from V1 to V3
343
+ }
344
+
345
+ private migrateFromV2(): void {
346
+ // Migration logic from V2 to V3
347
+ }
348
+ ```
349
+
350
+ Then pass the version when upgrading:
351
+
352
+ ```typescript
353
+ // In the upgrade transaction
354
+ const migrationData = new BytesWriter(8);
355
+ migrationData.writeU64(1); // Migrating from version 1
356
+
357
+ Blockchain.updateContractFromExisting(newBytecodeAddress, migrationData);
358
+ ```
359
+
360
+ ### Plugin Support
361
+
362
+ Plugins can also implement `onUpdate` to perform their own migration logic:
363
+
364
+ ```typescript
365
+ class MyPlugin extends Plugin {
366
+ public override onUpdate(calldata: Calldata): void {
367
+ // Plugin-specific migration logic
368
+ }
369
+ }
370
+ ```
371
+
372
+ ### Important Notes
373
+
374
+ 1. **onUpdate runs on new bytecode**: The hook executes using the new contract's code, not the old one
375
+ 2. **Storage is shared**: The new code reads/writes to the same storage slots as the old code
376
+ 3. **Call super.onUpdate()**: Always call `super.onUpdate(calldata)` to ensure plugins are notified
377
+ 4. **Empty calldata**: If no calldata was passed to `updateContractFromExisting`, an empty reader is provided
378
+
379
+ ## Security Considerations
380
+
381
+ ### Source Contract Validation
382
+
383
+ Always validate that the source address is an existing deployed contract:
384
+
385
+ ```typescript
386
+ if (!Blockchain.isContract(sourceAddress)) {
387
+ throw new Revert('Source must be a deployed contract');
388
+ }
389
+ ```
390
+
391
+ This prevents an attacker from:
392
+ 1. Submitting a not-yet-deployed address
393
+ 2. Deploying malicious bytecode just before applying
394
+ 3. Front-running the upgrade
395
+
396
+ ### Address Verification at Apply
397
+
398
+ The `applyUpgrade` function requires the address parameter to match the pending upgrade:
399
+
400
+ ```typescript
401
+ if (!sourceAddress.equals(this.pendingUpgradeAddress)) {
402
+ throw new Revert('Address does not match pending upgrade');
403
+ }
404
+ ```
405
+
406
+ This prevents front-running attacks where an attacker tries to substitute a different contract.
407
+
408
+ ### Permission Model
409
+
410
+ The VM does not enforce any specific permission model. Implement appropriate access control:
411
+
412
+ ```typescript
413
+ // Simple: Deployer only
414
+ this.onlyDeployer(Blockchain.tx.sender);
415
+
416
+ // Advanced: Multisig or governance
417
+ if (!this.isAuthorizedUpgrader(Blockchain.tx.sender)) {
418
+ throw new Revert('Not authorized');
419
+ }
420
+ ```
421
+
422
+ ### Activation Boundary
423
+
424
+ When `updateContractFromExisting` is called:
425
+ - Transactions in the **same block** execute against **old bytecode**
426
+ - Transactions in **subsequent blocks** execute against **new bytecode**
427
+
428
+ This provides a clean transition with no mid-block ambiguity.
429
+
430
+ ## Comparison with Other Platforms
431
+
432
+ | Feature | OPNet | Ethereum | Solana |
433
+ |---------|-------|----------|--------|
434
+ | Mechanism | VM opcode | Proxy + delegatecall | Upgrade authority |
435
+ | Delay | Contract-level (recommended) | Contract-level only | None (instant) |
436
+ | Storage | Same address, slots persist | Proxy storage, collision risk | Account data persists |
437
+ | Permission | Contract-defined | Proxy admin | Single authority key |
438
+ | Complexity | Low (single opcode) | High (proxy patterns) | Low (simple authority) |
439
+
440
+ ## Complete Example
441
+
442
+ ```typescript
443
+ import {
444
+ Upgradeable,
445
+ Blockchain,
446
+ Calldata,
447
+ BytesWriter,
448
+ encodeSelector,
449
+ Selector,
450
+ StoredU256,
451
+ EMPTY_POINTER,
452
+ ADDRESS_BYTE_LENGTH,
453
+ } from '@btc-vision/btc-runtime/runtime';
454
+
455
+ @final
456
+ export class UpgradeableVault extends Upgradeable {
457
+ // 1-week upgrade delay for security
458
+ protected readonly upgradeDelay: u64 = 1008;
459
+
460
+ // Storage pointers - never reorder these
461
+ private totalDepositedPointer: u16 = Blockchain.nextPointer;
462
+ private totalDeposited: StoredU256;
463
+
464
+ public constructor() {
465
+ super();
466
+ this.totalDeposited = new StoredU256(
467
+ this.totalDepositedPointer,
468
+ EMPTY_POINTER
469
+ );
470
+ }
471
+
472
+ public override execute(method: Selector, calldata: Calldata): BytesWriter {
473
+ switch (method) {
474
+ // Upgrade methods
475
+ case encodeSelector('submitUpgrade'):
476
+ return this.submitUpgrade(calldata.readAddress());
477
+ case encodeSelector('applyUpgrade'): {
478
+ const sourceAddress = calldata.readAddress();
479
+ const remainingLength = calldata.byteLength - ADDRESS_BYTE_LENGTH;
480
+ const updateCalldata = new BytesWriter(remainingLength);
481
+ if (remainingLength > 0) {
482
+ updateCalldata.writeBytes(calldata.readBytes(remainingLength));
483
+ }
484
+ return this.applyUpgrade(sourceAddress, updateCalldata);
485
+ }
486
+ case encodeSelector('cancelUpgrade'):
487
+ return this.cancelUpgrade();
488
+
489
+ // View methods for upgrade status
490
+ case encodeSelector('pendingUpgrade'):
491
+ return this.getPendingUpgrade();
492
+ case encodeSelector('upgradeEffectiveBlock'):
493
+ return this.getUpgradeEffectiveBlock();
494
+
495
+ // Business logic
496
+ case encodeSelector('deposit'):
497
+ return this.deposit(calldata);
498
+
499
+ default:
500
+ return super.execute(method, calldata);
501
+ }
502
+ }
503
+
504
+ private getPendingUpgrade(): BytesWriter {
505
+ const response = new BytesWriter(40);
506
+ response.writeAddress(this.pendingUpgradeAddress);
507
+ response.writeU64(this.pendingUpgradeBlock);
508
+ return response;
509
+ }
510
+
511
+ private getUpgradeEffectiveBlock(): BytesWriter {
512
+ const response = new BytesWriter(8);
513
+ response.writeU64(this.upgradeEffectiveBlock);
514
+ return response;
515
+ }
516
+
517
+ private deposit(calldata: Calldata): BytesWriter {
518
+ // Business logic...
519
+ return new BytesWriter(0);
520
+ }
521
+ }
522
+ ```
523
+
524
+ ## Upgrade Workflow
525
+
526
+ 1. **Develop and test new version** on testnet
527
+ 2. **Deploy new bytecode** as a separate contract
528
+ 3. **Submit upgrade** with `submitUpgrade(newAddress)`
529
+ 4. **Wait for delay** (users can exit during this period)
530
+ 5. **Apply upgrade** with `applyUpgrade(newAddress)`
531
+ 6. **Verify** new functionality works correctly
532
+
533
+ ---
534
+
535
+ **Navigation:**
536
+ - Previous: [Bitcoin Scripts](./bitcoin-scripts.md)
537
+ - Next: [Plugins](./plugins.md)