@openzeppelin/upgrades-core 1.25.0 → 1.26.0
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/artifacts/build-info.json +21465 -0
- package/dist/deployment.d.ts +28 -4
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +106 -25
- package/dist/deployment.js.map +1 -1
- package/dist/deployment.test.js +116 -0
- package/dist/deployment.test.js.map +1 -1
- package/dist/impl-store.d.ts +27 -2
- package/dist/impl-store.d.ts.map +1 -1
- package/dist/impl-store.js +39 -8
- package/dist/impl-store.js.map +1 -1
- package/dist/impl-store.test.js +30 -0
- package/dist/impl-store.test.js.map +1 -1
- package/dist/manifest.d.ts +3 -3
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.test.js +1 -0
- package/dist/manifest.test.js.map +1 -1
- package/dist/validate/error.d.ts +3 -0
- package/dist/validate/error.d.ts.map +1 -1
- package/dist/validate/error.js +7 -1
- package/dist/validate/error.js.map +1 -1
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +2 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/query.js +1 -1
- package/dist/validate/query.js.map +1 -1
- package/package.json +3 -2
- package/src/deployment.test.ts +182 -1
- package/src/deployment.ts +134 -26
- package/src/impl-store.test.ts +52 -1
- package/src/impl-store.ts +69 -11
- package/src/manifest.test.ts +1 -0
- package/src/manifest.ts +11 -8
- package/src/validate/error.ts +6 -0
- package/src/validate/index.ts +1 -1
- package/src/validate/query.ts +2 -2
package/src/deployment.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface Deployment {
|
|
|
22
22
|
txHash?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface RemoteDeploymentId {
|
|
26
|
+
remoteDeploymentId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for deploying an implementation contract, proxy admin contract, or remote deployment.
|
|
31
|
+
*/
|
|
25
32
|
export interface DeployOpts {
|
|
26
33
|
/**
|
|
27
34
|
* Timeout in milliseconds to wait for the transaction confirmation when deploying an implementation contract or proxy admin contract. Use `0` to wait indefinitely.
|
|
@@ -50,6 +57,7 @@ export interface DeployOpts {
|
|
|
50
57
|
* If not provided, invalid deployments will not be deleted in a dev network (which is not a problem if merge is false,
|
|
51
58
|
* since it will be overwritten with the new deployment).
|
|
52
59
|
* @param merge whether the cached deployment is intended to be merged with the new deployment. Defaults to false.
|
|
60
|
+
* @param getRemoteDeployment a function to get the remote deployment status by id. If the deployment id is not found, returns undefined.
|
|
53
61
|
* @returns the cached deployment if it should be used, otherwise the new deployment from the deploy function
|
|
54
62
|
* @throws {InvalidDeployment} if the cached deployment is invalid and we are not on a dev network
|
|
55
63
|
*/
|
|
@@ -61,8 +69,9 @@ export async function resumeOrDeploy<T extends Deployment, U extends T = T>(
|
|
|
61
69
|
opts?: DeployOpts,
|
|
62
70
|
deployment?: ManifestField<T>,
|
|
63
71
|
merge?: boolean,
|
|
72
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
64
73
|
): Promise<T | U> {
|
|
65
|
-
const validated = await validateCached(cached, provider, type, opts, deployment, merge);
|
|
74
|
+
const validated = await validateCached(cached, provider, type, opts, deployment, merge, getRemoteDeployment);
|
|
66
75
|
if (validated === undefined || merge) {
|
|
67
76
|
const deployment = await deploy();
|
|
68
77
|
debug('initiated deployment', 'transaction hash:', deployment.txHash, 'merge:', merge);
|
|
@@ -79,10 +88,11 @@ async function validateCached<T extends Deployment>(
|
|
|
79
88
|
opts?: DeployOpts,
|
|
80
89
|
deployment?: ManifestField<T>,
|
|
81
90
|
merge?: boolean,
|
|
91
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
82
92
|
): Promise<T | undefined> {
|
|
83
93
|
if (cached !== undefined) {
|
|
84
94
|
try {
|
|
85
|
-
await validateStoredDeployment(cached, provider, type, opts, merge);
|
|
95
|
+
return await validateStoredDeployment(cached, provider, type, opts, merge, getRemoteDeployment);
|
|
86
96
|
} catch (e) {
|
|
87
97
|
if (e instanceof InvalidDeployment && (await isDevelopmentNetwork(provider))) {
|
|
88
98
|
debug('ignoring invalid deployment in development network', e.deployment.address);
|
|
@@ -94,50 +104,79 @@ async function validateCached<T extends Deployment>(
|
|
|
94
104
|
throw e;
|
|
95
105
|
}
|
|
96
106
|
}
|
|
107
|
+
} else {
|
|
108
|
+
return undefined;
|
|
97
109
|
}
|
|
98
|
-
return cached;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
async function validateStoredDeployment<T extends Deployment>(
|
|
112
|
+
async function validateStoredDeployment<T extends Deployment & RemoteDeploymentId>(
|
|
102
113
|
stored: T,
|
|
103
114
|
provider: EthereumProvider,
|
|
104
115
|
type?: string,
|
|
105
116
|
opts?: DeployOpts,
|
|
106
117
|
merge?: boolean,
|
|
107
|
-
)
|
|
108
|
-
|
|
118
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
119
|
+
): Promise<T> {
|
|
120
|
+
const { txHash, remoteDeploymentId } = stored;
|
|
121
|
+
let deployment = stored;
|
|
122
|
+
|
|
109
123
|
if (txHash !== undefined) {
|
|
110
124
|
// If there is a deployment with txHash stored, we look its transaction up. If the
|
|
111
125
|
// transaction is found, the deployment is reused.
|
|
112
126
|
debug('found previous deployment', txHash);
|
|
113
127
|
const tx = await getTransactionByHash(provider, txHash);
|
|
128
|
+
let foundDeployment = undefined;
|
|
114
129
|
if (tx !== null) {
|
|
115
|
-
|
|
130
|
+
foundDeployment = txHash;
|
|
131
|
+
} else if (remoteDeploymentId !== undefined && getRemoteDeployment !== undefined) {
|
|
132
|
+
// If the transaction is not found, try checking the deployment id since the transaction may have been replaced
|
|
133
|
+
const response = await getRemoteDeployment(remoteDeploymentId);
|
|
134
|
+
if (response !== undefined) {
|
|
135
|
+
foundDeployment = remoteDeploymentId;
|
|
136
|
+
|
|
137
|
+
// update the stored tx hash
|
|
138
|
+
deployment = { ...stored, txHash: response.txHash };
|
|
139
|
+
debug('updating previous deployment to tx hash ', response.txHash);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (foundDeployment) {
|
|
144
|
+
debug('resuming previous deployment', foundDeployment);
|
|
116
145
|
if (merge) {
|
|
117
146
|
// If merging, wait for the existing deployment to be mined
|
|
118
|
-
waitAndValidateDeployment(provider,
|
|
147
|
+
waitAndValidateDeployment(provider, deployment, type, opts, getRemoteDeployment);
|
|
119
148
|
}
|
|
120
149
|
} else {
|
|
121
150
|
// If the transaction is not found we throw an error, except if we're in
|
|
122
151
|
// a development network then we simply silently redeploy.
|
|
123
152
|
// This error should be caught by the caller to determine if we're in a dev network.
|
|
124
|
-
throw new InvalidDeployment(
|
|
153
|
+
throw new InvalidDeployment(deployment);
|
|
125
154
|
}
|
|
126
155
|
} else {
|
|
127
|
-
const existingBytecode = await getCode(provider,
|
|
156
|
+
const existingBytecode = await getCode(provider, deployment.address);
|
|
128
157
|
if (isEmpty(existingBytecode)) {
|
|
129
|
-
throw new InvalidDeployment(
|
|
158
|
+
throw new InvalidDeployment(deployment);
|
|
130
159
|
}
|
|
131
160
|
}
|
|
161
|
+
return deployment;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A deployment that is performed remotely, which has a status and transaction hash.
|
|
166
|
+
*/
|
|
167
|
+
export interface RemoteDeployment {
|
|
168
|
+
status: 'completed' | 'failed' | 'submitted';
|
|
169
|
+
txHash: string;
|
|
132
170
|
}
|
|
133
171
|
|
|
134
172
|
export async function waitAndValidateDeployment(
|
|
135
173
|
provider: EthereumProvider,
|
|
136
|
-
deployment: Deployment,
|
|
174
|
+
deployment: Deployment & RemoteDeploymentId,
|
|
137
175
|
type?: string,
|
|
138
176
|
opts?: DeployOpts,
|
|
177
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
139
178
|
): Promise<void> {
|
|
140
|
-
const { txHash, address } = deployment;
|
|
179
|
+
const { txHash, address, remoteDeploymentId } = deployment;
|
|
141
180
|
|
|
142
181
|
// Poll for 60 seconds with a 5 second poll interval by default.
|
|
143
182
|
const pollTimeout = opts?.timeout ?? 60e3;
|
|
@@ -145,7 +184,38 @@ export async function waitAndValidateDeployment(
|
|
|
145
184
|
|
|
146
185
|
debug('polling timeout', pollTimeout, 'polling interval', pollInterval);
|
|
147
186
|
|
|
148
|
-
|
|
187
|
+
let foundCode = false;
|
|
188
|
+
|
|
189
|
+
if (remoteDeploymentId !== undefined && getRemoteDeployment !== undefined) {
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
|
|
192
|
+
// eslint-disable-next-line no-constant-condition
|
|
193
|
+
while (true) {
|
|
194
|
+
if (await hasCode(provider, address)) {
|
|
195
|
+
debug('code in target address found', address);
|
|
196
|
+
foundCode = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const completed = await isDeploymentCompleted(
|
|
201
|
+
address,
|
|
202
|
+
remoteDeploymentId,
|
|
203
|
+
await getRemoteDeployment(remoteDeploymentId),
|
|
204
|
+
);
|
|
205
|
+
if (completed) {
|
|
206
|
+
break;
|
|
207
|
+
} else {
|
|
208
|
+
await sleep(pollInterval);
|
|
209
|
+
}
|
|
210
|
+
if (pollTimeout != 0) {
|
|
211
|
+
const elapsedTime = Date.now() - startTime;
|
|
212
|
+
if (elapsedTime >= pollTimeout) {
|
|
213
|
+
// A timeout is NOT an InvalidDeployment
|
|
214
|
+
throw new TransactionMinedTimeout(deployment, type, !!opts);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else if (txHash !== undefined) {
|
|
149
219
|
const startTime = Date.now();
|
|
150
220
|
|
|
151
221
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -172,24 +242,28 @@ export async function waitAndValidateDeployment(
|
|
|
172
242
|
}
|
|
173
243
|
}
|
|
174
244
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
245
|
+
if (!foundCode) {
|
|
246
|
+
debug('verifying code in target address', address);
|
|
247
|
+
const startTime = Date.now();
|
|
248
|
+
while (!(await hasCode(provider, address))) {
|
|
249
|
+
const elapsedTime = Date.now() - startTime;
|
|
250
|
+
if (elapsedTime >= pollTimeout || txHash === undefined) {
|
|
251
|
+
throw new InvalidDeployment(deployment);
|
|
252
|
+
}
|
|
253
|
+
await sleep(pollInterval);
|
|
181
254
|
}
|
|
182
|
-
|
|
255
|
+
debug('code in target address found', address);
|
|
183
256
|
}
|
|
184
|
-
debug('code in target address found', address);
|
|
185
257
|
}
|
|
186
258
|
|
|
187
259
|
export class TransactionMinedTimeout extends UpgradesError {
|
|
188
|
-
constructor(readonly deployment: Deployment, type?: string, configurableTimeout?: boolean) {
|
|
260
|
+
constructor(readonly deployment: Deployment & RemoteDeploymentId, type?: string, configurableTimeout?: boolean) {
|
|
189
261
|
super(
|
|
190
|
-
`Timed out waiting for ${type ? type + ' ' : ''}contract deployment to address ${
|
|
191
|
-
deployment.
|
|
192
|
-
|
|
262
|
+
`Timed out waiting for ${type ? type + ' ' : ''}contract deployment to address ${deployment.address} with ${
|
|
263
|
+
deployment.remoteDeploymentId
|
|
264
|
+
? `deployment id ${deployment.remoteDeploymentId}`
|
|
265
|
+
: `transaction ${deployment.txHash}`
|
|
266
|
+
}`,
|
|
193
267
|
() =>
|
|
194
268
|
'Run the function again to continue waiting for the transaction confirmation.' +
|
|
195
269
|
(configurableTimeout
|
|
@@ -217,3 +291,37 @@ export class InvalidDeployment extends Error {
|
|
|
217
291
|
return msg;
|
|
218
292
|
}
|
|
219
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Checks if the deployment id is completed.
|
|
297
|
+
*
|
|
298
|
+
* @param address The expected address of the deployment.
|
|
299
|
+
* @param remoteDeploymentId The deployment id.
|
|
300
|
+
* @param remoteDeploymentResponse The remote deployment response corresponding to the given id.
|
|
301
|
+
* @returns true if the deployment id is completed, false otherwise.
|
|
302
|
+
* @throws {InvalidDeployment} if the deployment id failed.
|
|
303
|
+
*/
|
|
304
|
+
export async function isDeploymentCompleted(
|
|
305
|
+
address: string,
|
|
306
|
+
remoteDeploymentId: string,
|
|
307
|
+
remoteDeploymentResponse: RemoteDeployment | undefined,
|
|
308
|
+
): Promise<boolean> {
|
|
309
|
+
debug('verifying deployment id', remoteDeploymentId);
|
|
310
|
+
if (remoteDeploymentResponse === undefined) {
|
|
311
|
+
throw new Error(`Could not find remote deployment with id ${remoteDeploymentId}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const status = remoteDeploymentResponse.status;
|
|
315
|
+
if (status === 'completed') {
|
|
316
|
+
debug('succeeded verifying deployment id completed', remoteDeploymentId);
|
|
317
|
+
return true;
|
|
318
|
+
} else if (status === 'failed') {
|
|
319
|
+
debug(`deployment id ${remoteDeploymentId} failed with tx hash ${remoteDeploymentResponse.txHash}`);
|
|
320
|
+
throw new InvalidDeployment({ address, txHash: remoteDeploymentResponse.txHash });
|
|
321
|
+
} else if (status === 'submitted') {
|
|
322
|
+
debug('waiting for deployment id to be completed', remoteDeploymentId);
|
|
323
|
+
return false;
|
|
324
|
+
} else {
|
|
325
|
+
throw new Error(`Broken invariant: Unrecognized status ${status} for deployment id ${remoteDeploymentId}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
package/src/impl-store.test.ts
CHANGED
|
@@ -5,8 +5,9 @@ import rimrafAsync from 'rimraf';
|
|
|
5
5
|
import util from 'util';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import sinon from 'sinon';
|
|
8
9
|
|
|
9
|
-
import { fetchOrDeploy, mergeAddresses } from './impl-store';
|
|
10
|
+
import { fetchOrDeploy, fetchOrDeployGetDeployment, mergeAddresses } from './impl-store';
|
|
10
11
|
import { getVersion } from './version';
|
|
11
12
|
import { stubProvider } from './stub-provider';
|
|
12
13
|
import { ImplDeployment } from './manifest';
|
|
@@ -89,3 +90,53 @@ test('merge all addresses', async t => {
|
|
|
89
90
|
function unorderedEqual(arr1: string[], arr2: string[]) {
|
|
90
91
|
return arr1.every(i => arr2.includes(i)) && arr2.every(i => arr1.includes(i));
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
test('platform - replace tx hash for deployment', async t => {
|
|
95
|
+
const provider = stubProvider();
|
|
96
|
+
|
|
97
|
+
// create a pending deployment with id
|
|
98
|
+
const fakeDeploy = await provider.deployPending();
|
|
99
|
+
provider.removeContract(fakeDeploy.address);
|
|
100
|
+
async function fakeDeployWithId() {
|
|
101
|
+
return {
|
|
102
|
+
...fakeDeploy,
|
|
103
|
+
txHash: '0x1',
|
|
104
|
+
remoteDeploymentId: 'abc',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const getDeploymentResponse1 = sinon.stub().returns({
|
|
109
|
+
status: 'submitted',
|
|
110
|
+
txHash: '0x1',
|
|
111
|
+
});
|
|
112
|
+
// let it timeout
|
|
113
|
+
await t.throwsAsync(
|
|
114
|
+
fetchOrDeployGetDeployment(
|
|
115
|
+
version1,
|
|
116
|
+
provider,
|
|
117
|
+
fakeDeployWithId,
|
|
118
|
+
{ timeout: 1, pollingInterval: 0 },
|
|
119
|
+
undefined,
|
|
120
|
+
getDeploymentResponse1,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// make the contract code exist
|
|
125
|
+
provider.addContract(fakeDeploy.address);
|
|
126
|
+
|
|
127
|
+
// simulate a changed tx hash
|
|
128
|
+
const getDeploymentResponse2 = sinon.stub().returns({
|
|
129
|
+
status: 'completed',
|
|
130
|
+
txHash: '0x2',
|
|
131
|
+
});
|
|
132
|
+
const deployment = await fetchOrDeployGetDeployment(
|
|
133
|
+
version1,
|
|
134
|
+
provider,
|
|
135
|
+
fakeDeployWithId,
|
|
136
|
+
{ timeout: 1, pollingInterval: 0 },
|
|
137
|
+
undefined,
|
|
138
|
+
getDeploymentResponse2,
|
|
139
|
+
);
|
|
140
|
+
t.is(deployment.address, fakeDeploy.address);
|
|
141
|
+
t.is(deployment.txHash, '0x2');
|
|
142
|
+
});
|
package/src/impl-store.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import debug from './utils/debug';
|
|
2
2
|
import { Manifest, ManifestData, ImplDeployment } from './manifest';
|
|
3
3
|
import { EthereumProvider, isDevelopmentNetwork } from './provider';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Deployment,
|
|
6
|
+
RemoteDeploymentId,
|
|
7
|
+
RemoteDeployment,
|
|
8
|
+
InvalidDeployment,
|
|
9
|
+
resumeOrDeploy,
|
|
10
|
+
waitAndValidateDeployment,
|
|
11
|
+
} from './deployment';
|
|
5
12
|
import { Version } from './version';
|
|
6
13
|
import assert from 'assert';
|
|
7
14
|
import { DeployOpts } from '.';
|
|
@@ -26,6 +33,7 @@ export interface ManifestField<T> {
|
|
|
26
33
|
* @param deploy the deploy function
|
|
27
34
|
* @param opts options containing the timeout and pollingInterval parameters. If undefined, assumes the timeout is not configurable and will not mention those parameters in the error message for TransactionMinedTimeout.
|
|
28
35
|
* @param merge if true, adds a deployment to existing deployment by merging their addresses. Defaults to false.
|
|
36
|
+
* @param getRemoteDeployment a function to get the remote deployment status by id. If the deployment id is not found, returns undefined.
|
|
29
37
|
* @returns the deployment
|
|
30
38
|
* @throws {InvalidDeployment} if the deployment is invalid
|
|
31
39
|
* @throws {TransactionMinedTimeout} if the transaction was not confirmed within the timeout period
|
|
@@ -36,6 +44,7 @@ async function fetchOrDeployGeneric<T extends Deployment, U extends T = T>(
|
|
|
36
44
|
deploy: () => Promise<U>,
|
|
37
45
|
opts?: DeployOpts,
|
|
38
46
|
merge?: boolean,
|
|
47
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
39
48
|
): Promise<U | Deployment> {
|
|
40
49
|
const manifest = await Manifest.forNetwork(provider);
|
|
41
50
|
|
|
@@ -51,7 +60,16 @@ async function fetchOrDeployGeneric<T extends Deployment, U extends T = T>(
|
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
const stored = deployment.get();
|
|
54
|
-
const updated = await resumeOrDeploy(
|
|
63
|
+
const updated = await resumeOrDeploy(
|
|
64
|
+
provider,
|
|
65
|
+
stored,
|
|
66
|
+
deploy,
|
|
67
|
+
lens.type,
|
|
68
|
+
opts,
|
|
69
|
+
deployment,
|
|
70
|
+
merge,
|
|
71
|
+
getRemoteDeployment,
|
|
72
|
+
);
|
|
55
73
|
if (updated !== stored) {
|
|
56
74
|
if (merge && deployment.merge) {
|
|
57
75
|
// only check primary addresses for clashes, since the address could already exist in an allAddresses field
|
|
@@ -67,7 +85,7 @@ async function fetchOrDeployGeneric<T extends Deployment, U extends T = T>(
|
|
|
67
85
|
return updated;
|
|
68
86
|
});
|
|
69
87
|
|
|
70
|
-
await waitAndValidateDeployment(provider, deployment, lens.type, opts);
|
|
88
|
+
await waitAndValidateDeployment(provider, deployment, lens.type, opts, getRemoteDeployment);
|
|
71
89
|
|
|
72
90
|
return deployment;
|
|
73
91
|
} catch (e) {
|
|
@@ -98,6 +116,18 @@ export function deleteDeployment(deployment: ManifestField<Deployment>) {
|
|
|
98
116
|
deployment.set(undefined);
|
|
99
117
|
}
|
|
100
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Fetches the deployment address from the manifest, or deploys it if not found and returns the address.
|
|
121
|
+
*
|
|
122
|
+
* @param version the contract version
|
|
123
|
+
* @param provider the Ethereum provider
|
|
124
|
+
* @param deploy the deploy function
|
|
125
|
+
* @param opts options containing the timeout and pollingInterval parameters. If undefined, assumes the timeout is not configurable and will not mention those parameters in the error message for TransactionMinedTimeout.
|
|
126
|
+
* @param merge if true, adds a deployment to existing deployment by merging their addresses. Defaults to false.
|
|
127
|
+
* @returns the deployment address
|
|
128
|
+
* @throws {InvalidDeployment} if the deployment is invalid
|
|
129
|
+
* @throws {TransactionMinedTimeout} if the transaction was not confirmed within the timeout period
|
|
130
|
+
*/
|
|
101
131
|
export async function fetchOrDeploy(
|
|
102
132
|
version: Version,
|
|
103
133
|
provider: EthereumProvider,
|
|
@@ -108,14 +138,35 @@ export async function fetchOrDeploy(
|
|
|
108
138
|
return (await fetchOrDeployGeneric(implLens(version.linkedWithoutMetadata), provider, deploy, opts, merge)).address;
|
|
109
139
|
}
|
|
110
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Fetches the deployment from the manifest, or deploys it if not found and returns the deployment.
|
|
143
|
+
*
|
|
144
|
+
* @param version the contract version
|
|
145
|
+
* @param provider the Ethereum provider
|
|
146
|
+
* @param deploy the deploy function
|
|
147
|
+
* @param opts options containing the timeout and pollingInterval parameters. If undefined, assumes the timeout is not configurable and will not mention those parameters in the error message for TransactionMinedTimeout.
|
|
148
|
+
* @param merge if true, adds a deployment to existing deployment by merging their addresses. Defaults to false.
|
|
149
|
+
* @param getRemoteDeployment a function to get the remote deployment status by id. If the deployment id is not found, returns undefined.
|
|
150
|
+
* @returns the deployment
|
|
151
|
+
* @throws {InvalidDeployment} if the deployment is invalid
|
|
152
|
+
* @throws {TransactionMinedTimeout} if the transaction was not confirmed within the timeout period
|
|
153
|
+
*/
|
|
111
154
|
export async function fetchOrDeployGetDeployment<T extends ImplDeployment>(
|
|
112
155
|
version: Version,
|
|
113
156
|
provider: EthereumProvider,
|
|
114
157
|
deploy: () => Promise<T>,
|
|
115
158
|
opts?: DeployOpts,
|
|
116
159
|
merge?: boolean,
|
|
160
|
+
getRemoteDeployment?: (remoteDeploymentId: string) => Promise<RemoteDeployment | undefined>,
|
|
117
161
|
): Promise<T | Deployment> {
|
|
118
|
-
return fetchOrDeployGeneric(
|
|
162
|
+
return fetchOrDeployGeneric(
|
|
163
|
+
implLens(version.linkedWithoutMetadata),
|
|
164
|
+
provider,
|
|
165
|
+
deploy,
|
|
166
|
+
opts,
|
|
167
|
+
merge,
|
|
168
|
+
getRemoteDeployment,
|
|
169
|
+
);
|
|
119
170
|
}
|
|
120
171
|
|
|
121
172
|
const implLens = (versionWithoutMetadata: string) =>
|
|
@@ -171,7 +222,7 @@ function lens<T>(description: string, type: string, fn: (data: ManifestData) =>
|
|
|
171
222
|
async function checkForAddressClash(
|
|
172
223
|
provider: EthereumProvider,
|
|
173
224
|
data: ManifestData,
|
|
174
|
-
updated: Deployment,
|
|
225
|
+
updated: Deployment & RemoteDeploymentId,
|
|
175
226
|
checkAllAddresses: boolean,
|
|
176
227
|
): Promise<void> {
|
|
177
228
|
const clash = lookupDeployment(data, updated.address, checkAllAddresses);
|
|
@@ -180,11 +231,18 @@ async function checkForAddressClash(
|
|
|
180
231
|
debug('deleting a previous deployment at address', updated.address);
|
|
181
232
|
clash.set(undefined);
|
|
182
233
|
} else {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
234
|
+
const existing = clash.get();
|
|
235
|
+
// it's a clash if there is no deployment id or if deployment ids don't match
|
|
236
|
+
if (
|
|
237
|
+
existing !== undefined &&
|
|
238
|
+
(existing.remoteDeploymentId === undefined || existing.remoteDeploymentId !== updated.remoteDeploymentId)
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`The following deployment clashes with an existing one at ${updated.address}\n\n` +
|
|
242
|
+
JSON.stringify(updated, null, 2) +
|
|
243
|
+
`\n\n`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
188
246
|
}
|
|
189
247
|
}
|
|
190
248
|
}
|
|
@@ -193,7 +251,7 @@ function lookupDeployment(
|
|
|
193
251
|
data: ManifestData,
|
|
194
252
|
address: string,
|
|
195
253
|
checkAllAddresses: boolean,
|
|
196
|
-
): ManifestField<Deployment> | undefined {
|
|
254
|
+
): ManifestField<Deployment & RemoteDeploymentId> | undefined {
|
|
197
255
|
if (data.admin?.address === address) {
|
|
198
256
|
return adminLens(data);
|
|
199
257
|
}
|
package/src/manifest.test.ts
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { EthereumProvider, getChainId, getHardhatMetadata, networkNames } from '
|
|
|
5
5
|
import lockfile from 'proper-lockfile';
|
|
6
6
|
import { compare as compareVersions } from 'compare-versions';
|
|
7
7
|
|
|
8
|
-
import type { Deployment } from './deployment';
|
|
8
|
+
import type { Deployment, RemoteDeploymentId } from './deployment';
|
|
9
9
|
import type { StorageLayout } from './storage';
|
|
10
10
|
import { pick } from './utils/pick';
|
|
11
11
|
import { mapValues } from './utils/map-values';
|
|
@@ -18,9 +18,9 @@ const currentManifestVersion = '3.2';
|
|
|
18
18
|
export interface ManifestData {
|
|
19
19
|
manifestVersion: string;
|
|
20
20
|
impls: {
|
|
21
|
-
[version in string]?: ImplDeployment;
|
|
21
|
+
[version in string]?: ImplDeployment & RemoteDeploymentId;
|
|
22
22
|
};
|
|
23
|
-
proxies: ProxyDeployment[];
|
|
23
|
+
proxies: (ProxyDeployment & RemoteDeploymentId)[];
|
|
24
24
|
admin?: Deployment;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -318,11 +318,14 @@ export function normalizeManifestData(input: ManifestData): ManifestData {
|
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
function normalizeDeployment<D extends Deployment>(input: D): Deployment;
|
|
322
|
-
function normalizeDeployment<D extends Deployment, K extends keyof D>(
|
|
323
|
-
|
|
321
|
+
function normalizeDeployment<D extends Deployment & RemoteDeploymentId>(input: D): Deployment & RemoteDeploymentId;
|
|
322
|
+
function normalizeDeployment<D extends Deployment & RemoteDeploymentId, K extends keyof D>(
|
|
323
|
+
input: D,
|
|
324
|
+
include: K[],
|
|
325
|
+
): Deployment & RemoteDeploymentId & Pick<D, K>;
|
|
326
|
+
function normalizeDeployment<D extends Deployment & RemoteDeploymentId, K extends keyof D>(
|
|
324
327
|
input: D,
|
|
325
328
|
include: K[] = [],
|
|
326
|
-
): Deployment & Pick<D, K> {
|
|
327
|
-
return pick(input, ['address', 'txHash', ...include]);
|
|
329
|
+
): Deployment & RemoteDeploymentId & Pick<D, K> {
|
|
330
|
+
return pick(input, ['address', 'txHash', 'remoteDeploymentId', ...include]);
|
|
328
331
|
}
|
package/src/validate/error.ts
CHANGED
|
@@ -9,3 +9,9 @@ export class ValidationErrors extends UpgradesError {
|
|
|
9
9
|
);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export class ContractSourceNotFoundError extends UpgradesError {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('The requested contract was not found. Make sure the source code is available for compilation');
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/validate/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { validate, ValidationRunData, ContractValidation } from './run';
|
|
2
2
|
export { ProxyKindOption, StandaloneValidationOptions, ValidationOptions, withValidationDefaults } from './overrides';
|
|
3
|
-
export { ValidationErrors } from './error';
|
|
3
|
+
export { ValidationErrors, ContractSourceNotFoundError } from './error';
|
|
4
4
|
export { RunValidation, ValidationLog, Validation, ValidationResult } from './compat';
|
|
5
5
|
export { ValidationData, ValidationDataCurrent, isCurrentValidationData, concatRunData } from './data';
|
|
6
6
|
export {
|
package/src/validate/query.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ValidationRunData, ValidationError, isOpcodeError } from './run';
|
|
|
3
3
|
import { StorageLayout } from '../storage/layout';
|
|
4
4
|
import { unlinkBytecode } from '../link-refs';
|
|
5
5
|
import { ValidationOptions, processExceptions } from './overrides';
|
|
6
|
-
import { ValidationErrors } from './error';
|
|
6
|
+
import { ContractSourceNotFoundError, ValidationErrors } from './error';
|
|
7
7
|
import { ValidationData, normalizeValidationData } from './data';
|
|
8
8
|
import { ProxyDeployment } from '../manifest';
|
|
9
9
|
|
|
@@ -72,7 +72,7 @@ export function getContractNameAndRunValidation(data: ValidationData, version: V
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (fullContractName === undefined || runValidation === undefined) {
|
|
75
|
-
throw new
|
|
75
|
+
throw new ContractSourceNotFoundError();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
return [fullContractName, runValidation];
|