Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Commit

Permalink
Merge pull request #200 from ar-io/pe-5428-delegated-staking-v2
Browse files Browse the repository at this point in the history
feat(PE-5428): delegated staking v2
  • Loading branch information
dtfiedler authored Feb 15, 2024
2 parents b1485aa + 2fd70d9 commit 24d7a12
Show file tree
Hide file tree
Showing 34 changed files with 3,396 additions and 146 deletions.
6 changes: 6 additions & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const {
increaseVaultSchema,
saveObservationsSchema,
updateGatewaySchema,
delegateStakeSchema,
decreaseDelegateStakeSchema,
} = require('./schemas');

// build our validation source code
Expand All @@ -34,6 +36,8 @@ const ajv = new Ajv({
increaseVaultSchema,
saveObservationsSchema,
updateGatewaySchema,
delegateStakeSchema,
decreaseDelegateStakeSchema,
],
code: { source: true, esm: true },
allErrors: true,
Expand All @@ -52,6 +56,8 @@ const moduleCode = standaloneCode(ajv, {
validateIncreaseVault: '#/definitions/increaseVault',
validateSaveObservations: '#/definitions/saveObservations',
validateUpdateGateway: '#/definitions/updateGateway',
validateDelegateStake: '#/definitions/delegateStake',
validateDecreaseDelegateStake: '#/definitions/decreaseDelegateStake',
});

// Now you can write the module code to file
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
setupFilesAfterEnv: ['./tests/mocks.jest.ts'],
testMatch: ['**/src/**/*.test.ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/tests/**'],
testEnvironment: 'node',
testTimeout: 60_000,
transform: {
Expand Down
24 changes: 24 additions & 0 deletions schemas/decreaseDelegateStake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const decreaseDelegateStakeSchema = {
$id: '#/definitions/decreaseDelegateStake',
type: 'object',
properties: {
function: {
type: 'string',
const: 'delegateStake',
},
target: {
type: 'string',
pattern: '^[a-zA-Z0-9-_]{43}$',
},
qty: {
type: 'integer',
minimum: 1,
},
},
required: ['target', 'qty'],
additionalProperties: false,
};

module.exports = {
decreaseDelegateStakeSchema,
};
24 changes: 24 additions & 0 deletions schemas/delegateStake.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const delegateStakeSchema = {
$id: '#/definitions/delegateStake',
type: 'object',
properties: {
function: {
type: 'string',
const: 'delegateStake',
},
target: {
type: 'string',
pattern: '^[a-zA-Z0-9-_]{43}$',
},
qty: {
type: 'integer',
minimum: 1,
},
},
required: ['target', 'qty'],
additionalProperties: false,
};

module.exports = {
delegateStakeSchema,
};
5 changes: 5 additions & 0 deletions schemas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const { extendVaultSchema } = require('./extendVault');
const { increaseVaultSchema } = require('./increaseVault');
const { saveObservationsSchema } = require('./saveObservations');
const { updateGatewaySchema } = require('./updateGateway');
const { delegateStakeSchema } = require('./delegateStake');
const { decreaseDelegateStakeSchema } = require('./decreaseDelegateStake');

module.exports = {
auctionBidSchema,
buyRecordSchema,
Expand All @@ -23,4 +26,6 @@ module.exports = {
increaseVaultSchema,
saveObservationsSchema,
updateGatewaySchema,
delegateStakeSchema,
decreaseDelegateStakeSchema,
};
26 changes: 25 additions & 1 deletion schemas/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const joinNetworkSchema = {
pattern: '^(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{1,63}$', // eslint-disable-line no-useless-escape
},
port: {
type: 'number',
type: 'integer',
minimum: 0,
maximum: 65535,
},
Expand All @@ -39,6 +39,30 @@ const joinNetworkSchema = {
type: 'string',
pattern: '^[a-zA-Z0-9_-]{43}$',
},
allowDelegatedStaking: {
type: 'boolean',
},
delegateRewardShareRatio: {
type: 'integer',
minimum: 0,
maximum: 100,
},
allowedDelegates: {
type: 'array',
items: {
type: 'string',
pattern: '^[a-zA-Z0-9-_]{43}$',
description:
'The unique list of delegate addresses the that can stake on this gateway',
},
uniqueItems: true,
minItems: 0,
maxItems: 10_000,
},
minDelegatedStake: {
type: 'integer',
minimum: 100,
},
},
required: ['qty', 'fqdn', 'port', 'protocol', 'properties', 'note', 'label'],
additionalProperties: false,
Expand Down
12 changes: 12 additions & 0 deletions schemas/updateGateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ const updateGatewaySchema = {
type: 'string',
pattern: '^(|[a-zA-Z0-9_-]{43})$',
},
allowDelegatedStaking: {
type: 'boolean',
},
delegateRewardShareRatio: {
type: 'integer',
minimum: 0,
maximum: 100,
},
minDelegatedStake: {
type: 'integer',
minimum: 100,
},
},
required: [],
additionalProperties: false,
Expand Down
170 changes: 170 additions & 0 deletions src/actions/write/decreaseDelegateStake.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import {
DELEGATED_STAKE_UNLOCK_LENGTH,
INVALID_INPUT_MESSAGE,
} from '../../constants';
import {
getBaselineState,
stubbedArweaveTxId,
stubbedGatewayData,
} from '../../tests/stubs';
import { IOState } from '../../types';
import { decreaseDelegateStake } from './decreaseDelegateStake';

describe('decreaseDelegateStake', () => {
describe('invalid inputs', () => {
it.each([[0, '', stubbedArweaveTxId.concat(stubbedArweaveTxId), true]])(
'should throw an error on invalid target',
async (badLabel: unknown) => {
const initialState: IOState = {
...getBaselineState(),
gateways: {
[stubbedArweaveTxId]: {
...stubbedGatewayData,
},
},
};
const error = await decreaseDelegateStake(initialState, {
caller: stubbedArweaveTxId,
input: {
label: badLabel,
},
}).catch((e: any) => e);

Check warning on line 31 in src/actions/write/decreaseDelegateStake.test.ts

View workflow job for this annotation

GitHub Actions / build / build (lint:check)

Unexpected any. Specify a different type
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual(
expect.stringContaining(INVALID_INPUT_MESSAGE),
);
},
);

it.each([['bad-port', '0', 0, -1, true, Number.MAX_SAFE_INTEGER]])(
'should throw an error on invalid qty',
async (badQty: unknown) => {
const initialState = getBaselineState();
const error = await decreaseDelegateStake(initialState, {
caller: 'test',
input: {
qty: badQty,
settings: {
port: badQty,
},
},
}).catch((e) => e);
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual(
expect.stringContaining(INVALID_INPUT_MESSAGE),
);
},
);
});

describe('valid inputs', () => {
it('should decrease the delegate stake', async () => {
const initialState: IOState = {
...getBaselineState(),
gateways: {
[stubbedArweaveTxId]: {
...stubbedGatewayData,
delegates: {
['existing-delegate']: {
delegatedStake: 1000,
start: 0,
vaults: {},
},
},
},
},
};
const { state } = await decreaseDelegateStake(initialState, {
caller: 'existing-delegate',
input: {
target: stubbedArweaveTxId,
qty: 500,
},
});
const expectedDecreasedDelegateData = {
delegatedStake: 500,
start: 0,
vaults: {
[SmartWeave.transaction.id]: {
balance: 500,
start: SmartWeave.block.height,
end: SmartWeave.block.height + DELEGATED_STAKE_UNLOCK_LENGTH,
},
},
};
expect(
state.gateways[stubbedArweaveTxId].delegates['existing-delegate'],
).toEqual(expectedDecreasedDelegateData);
});
});

it('should error if the decrease would lower the delegate stake than the required minimum stake', async () => {
const initialState: IOState = {
...getBaselineState(),
gateways: {
[stubbedArweaveTxId]: {
...stubbedGatewayData,
delegates: {
['existing-delegate']: {
delegatedStake: 1000,
start: 0,
vaults: {},
},
},
},
},
};
const error = await decreaseDelegateStake(initialState, {
caller: 'existing-delegate',
input: {
target: stubbedArweaveTxId,
qty: 901,
},
}).catch((e) => e);
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual(
expect.stringContaining(
'Remaining delegated stake must be greater than the minimum delegated stake amount.',
),
);
});

it('should move the delegate stake to a vault if the decrease amount equals the current amount delegated', async () => {
const initialState: IOState = {
...getBaselineState(),
gateways: {
[stubbedArweaveTxId]: {
...stubbedGatewayData,
delegates: {
['existing-delegate']: {
delegatedStake: 1000,
start: 0,
vaults: {},
},
},
},
},
};
const { state } = await decreaseDelegateStake(initialState, {
caller: 'existing-delegate',
input: {
target: stubbedArweaveTxId,
qty: 1000,
},
});
const expectedDecreasedDelegateData = {
delegatedStake: 0,
start: 0,
vaults: {
[SmartWeave.transaction.id]: {
balance: 1000,
start: SmartWeave.block.height,
end: SmartWeave.block.height + DELEGATED_STAKE_UNLOCK_LENGTH,
},
},
};
expect(
state.gateways[stubbedArweaveTxId].delegates['existing-delegate'],
).toEqual(expectedDecreasedDelegateData);
});
});
49 changes: 49 additions & 0 deletions src/actions/write/decreaseDelegateStake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { safeDecreaseDelegateStake } from '../../delegates';
import {
BlockHeight,
ContractWriteResult,
IOState,
IOToken,
PstAction,
} from '../../types';
import { getInvalidAjvMessage } from '../../utilities';
import { validateDecreaseDelegateStake } from '../../validations';

export class DecreaseDelegateStake {
target: string;
qty: IOToken;

constructor(input: any) {

Check warning on line 16 in src/actions/write/decreaseDelegateStake.ts

View workflow job for this annotation

GitHub Actions / build / build (lint:check)

Argument 'input' should be typed with a non-any type
if (!validateDecreaseDelegateStake(input)) {
throw new ContractError(
getInvalidAjvMessage(
validateDecreaseDelegateStake,
input,
'decreaseDelegateStake',
),
);
}
const { target, qty } = input;
this.target = target;
this.qty = new IOToken(qty);
}
}

export const decreaseDelegateStake = async (
state: IOState,
{ caller, input }: PstAction,
): Promise<ContractWriteResult> => {
const { gateways } = state;
const { target, qty } = new DecreaseDelegateStake(input);

safeDecreaseDelegateStake({
gateways,
fromAddress: caller,
gatewayAddress: target,
qty,
id: SmartWeave.transaction.id,
startHeight: new BlockHeight(SmartWeave.block.height),
});

return { state };
};
Loading

0 comments on commit 24d7a12

Please sign in to comment.