Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alpha #1256

Merged
merged 33 commits into from
Jul 30, 2024
Merged

Alpha #1256

Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7858460
ci: 🎡 Add action to authenticate commits
prashantasdeveloper Jun 5, 2024
290fa75
refactor: 💡 rename secret to MIDDLEWARE_ALLOWED_SIGNERS
prashantasdeveloper Jun 7, 2024
23fbab1
chore: 🤖 use variable instead of secret for allowed signers
prashantasdeveloper Jun 7, 2024
e06ac80
feat: 🎸 Add support for off-chain settlements
prashantasdeveloper Jun 10, 2024
7d50d0f
feat: 🎸 Add method to get allowed signers for a venue
prashantasdeveloper Jun 11, 2024
dc3ae78
feat: 🎸 Add methods to get off chain affirmations
prashantasdeveloper Jun 11, 2024
8845de9
feat: 🎸 Add method to get off chain receipts used by an account
prashantasdeveloper Jun 11, 2024
fc066ad
test: 💍 add test for Account.getOffChainReceipts
prashantasdeveloper Jun 12, 2024
dcc255e
test: 💍 add coverage for Instruction entity
prashantasdeveloper Jun 12, 2024
1948385
test: 💍 add unit test for Venue.getAllowedSigners
prashantasdeveloper Jun 12, 2024
829170f
test: 💍 add unit tests for addInstruction procedure
prashantasdeveloper Jun 12, 2024
17e040c
test: 💍 add unit test for executeManualInstruction procedure
prashantasdeveloper Jun 12, 2024
19bd7c7
test: 💍 Add tests for instruction affimration with receipts
prashantasdeveloper Jun 12, 2024
6a8f1bb
test: 💍 add unit tests for conversion methods
prashantasdeveloper Jun 12, 2024
e648cce
test: 💍 handle FungibleLeg type conversion
prashantasdeveloper Jun 12, 2024
d4e79e9
test: 💍 add unit test for legToOffChainLeg
prashantasdeveloper Jun 12, 2024
1f279b8
feat: 🎸 Add method to generate offchain affirmation receipt
prashantasdeveloper Jun 14, 2024
52c77ee
test: 💍 resolve sonar issue
prashantasdeveloper Jun 14, 2024
b6da75f
feat: 🎸 Add option to add/remove venue signers
prashantasdeveloper Jun 19, 2024
2789470
feat: 🎸 remove updated_at in queries
polymath-eric Jun 21, 2024
c3761c4
feat: 🎸 Use `currentAssetMetadataLocalKey` to get next local ID
prashantasdeveloper May 29, 2024
4920a51
feat: 🎸 Bump supported spec version to 6.3
prashantasdeveloper Jun 20, 2024
7612431
fix: 🐛 get portfolio transactions error
sansan Jun 20, 2024
699fad7
feat: 🎸 allow MultiSig signers to submit proposals
polymath-eric Jun 27, 2024
1af7911
feat: 🎸 add `MultiSig.joinCreator` method
polymath-eric Jul 9, 2024
f72cb68
fix: 🐛 `multiSig.getHistoricalProposal` errors
polymath-eric Jul 11, 2024
8689f63
docs: ✏️ remove @hidden from procedure .multiSig public property
polymath-eric Jul 11, 2024
8f120ca
fix: 🐛 throw catchable connect errors
polymath-eric Jul 15, 2024
e8b1319
docs: ✏️ fix readme type badge link
polymath-eric Jul 16, 2024
5053f00
feat: 🎸 Add procedure to add secondary accounts to an Identity
prashantasdeveloper Jul 18, 2024
8496491
feat: 🎸 Add procedure to create multiple child identities
prashantasdeveloper Jul 19, 2024
dcc13a3
feat: 🎸 Add support for settlements V2
prashantasdeveloper Jul 11, 2024
d0de49c
chore: 🤖 modify queries to work with SQ historical state
prashantasdeveloper Jul 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/authenticate-commits.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Authenticate Commits
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Import allowed SSH keys
env:
ALLOWED_SIGNERS: ${{ vars.MIDDLEWARE_ALLOWED_SIGNERS }}
run: |
mkdir -p ~/.ssh
echo "$ALLOWED_SIGNERS" > ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile "~/.ssh/allowed_signers"

- name: Validate commit signatures
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Function to verify a commit
verify_commit() {
local commit=$1
local status=$(git show --pretty="format:%G?" $commit | head -n 1)

if [ "$status" != "G" ]; then
local committer=$(git log -1 --pretty=format:'%cn (%ce)' $commit)
echo "Commit $commit from $committer has an invalid signature or is not signed by an allowed key."
exit 1
fi

}

# Get all commits in the PR
commits=$(git rev-list $BASE_SHA..$HEAD_SHA)

# Iterate over all commits in the PR and verify each one
for COMMIT in $commits; do
verify_commit $COMMIT
done

echo "All commits are signed with allowed keys."
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ Creating transactions is a two-step process. First a procedure is created, which
const newAsset = await createAssetProc.run()
```

#### Creating MultiSig Proposals

If the signingAccount is a MultiSig signer, then the transaction will need to be ran with `.runAsProposal()` instead of the usual `.run()`.
The underlying transaction will be wrapped with `multiSig.createProposalAsKey` extrinsic and will resolve to the MultiSig proposal created.

Approving and rejecting existing proposals are an exception and should be submitted with `.run()`. If your application supports
MultiSig signers, then the procedure's `multiSig` param can be checked to ensure the correct method is called.

```typescript
const createAssetProc = await polyClient.assets.createAsset(
args,
{
signingAccount: multiSigSigner
}
)
createAssetProc.multiSig // indicates the acting MultiSig. If set `runAsProposal` must be used
const proposal = await createAssetProc.runAsProposal()

const rejectProc = await proposal.reject({ signingAccount: multiSigSigner })
rejectProc.multiSig // returns `null`. Rejecting a proposal does not get wrapped
await rejectProc.run()
```

#### Reading Data

The SDK exposes getter functions that will return entities, which may have their own functions:
Expand Down
34 changes: 24 additions & 10 deletions src/api/entities/Account/MultiSig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import BigNumber from 'bignumber.js';

import { UniqueIdentifiers } from '~/api/entities/Account';
import { MultiSigProposal } from '~/api/entities/MultiSigProposal';
import { Account, Context, Identity, modifyMultiSig, PolymeshError } from '~/internal';
import { Account, Context, Identity, joinCreator, modifyMultiSig, PolymeshError } from '~/internal';
import { multiSigProposalsQuery } from '~/middleware/queries';
import { Query } from '~/middleware/types';
import {
ErrorCode,
JoinCreatorParams,
ModifyMultiSigParams,
MultiSigDetails,
OptionalArgsProcedureMethod,
ProcedureMethod,
ProposalStatus,
ResultSet,
Expand Down Expand Up @@ -41,6 +43,13 @@ export class MultiSig extends Account {
},
context
);
this.joinCreator = createProcedureMethod(
{
getProcedureAndArgs: joinArgs => [joinCreator, { multiSig: this, ...joinArgs }],
optionalArgs: true,
},
context
);
}

/**
Expand Down Expand Up @@ -148,26 +157,22 @@ export class MultiSig extends Account {
}

/**
* Return all { @link api/entities/MultiSigProposal!MultiSigProposal } for this MultiSig Account
* Return a set of { @link api/entities/MultiSigProposal!MultiSigProposal | MultiSigProposal } for this MultiSig Account
*
* @note uses the middlewareV2
*/
public async getHistoricalProposals(opts: {
public async getHistoricalProposals(opts?: {
size?: BigNumber;
start?: BigNumber;
}): Promise<ResultSet<MultiSigProposal>> {
const {
context: { queryMiddleware },
context,
address,
} = this;
const { size, start } = opts;
const { context, address } = this;
const { size, start } = opts ?? {};

const {
data: {
multiSigProposals: { nodes, totalCount },
},
} = await queryMiddleware<Ensured<Query, 'multiSigProposals'>>(
} = await context.queryMiddleware<Ensured<Query, 'multiSigProposals'>>(
multiSigProposalsQuery(address, size, start)
);

Expand Down Expand Up @@ -219,4 +224,13 @@ export class MultiSig extends Account {
* Modify the signers for the MultiSig. The signing Account must belong to the Identity of the creator of the MultiSig
*/
public modify: ProcedureMethod<Pick<ModifyMultiSigParams, 'signers'>, void>;

/**
* Attach a MultiSig directly to the creator's identity. This method bypasses the usual authorization step to join an identity
*
* @note the caller should be the MultiSig creator's primary key
*
* @note To attach the MultiSig to an identity other than the creator's, {@link api/client/AccountManagement!AccountManagement.inviteAccount | inviteAccount} can be used instead. The MultiSig will then need to accept the created authorization
*/
public joinCreator: OptionalArgsProcedureMethod<JoinCreatorParams, void>;
}
38 changes: 29 additions & 9 deletions src/api/entities/Account/__tests__/MultiSig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe('MultiSig class', () => {
nodes: [mockHistoricalMultisig],
};

it('should get proposals', async () => {
it('should get historical proposals', async () => {
dsMockUtils.createApolloQueryMock(
multiSigProposalsQuery(address, new BigNumber(1), new BigNumber(0)),
{
Expand All @@ -237,14 +237,17 @@ describe('MultiSig class', () => {
expect(data.length).toEqual(1);
});

it('should return an empty array if no proposals are pending', async () => {
dsMockUtils.createQueryMock('multiSig', 'proposals', {
entries: [],
it('should work with optional pagination params', async () => {
dsMockUtils.createApolloQueryMock(multiSigProposalsQuery(address), {
multiSigProposals: multiSigProposalsResponse,
});
const result = await multiSig.getHistoricalProposals();

const result = await multiSig.getProposals();
const { data, next, count } = result;

expect(result).toEqual([]);
expect(next).toEqual(new BigNumber(1));
expect(count).toEqual(new BigNumber(2));
expect(data.length).toEqual(1);
});
});

Expand Down Expand Up @@ -274,7 +277,7 @@ describe('MultiSig class', () => {

describe('method: modify', () => {
const account = entityMockUtils.getAccountInstance({ address });
it('should prepare the procedure and return the resulting transaction queue', async () => {
it('should prepare the procedure and return the resulting procedure', async () => {
const expectedTransaction = 'someQueue' as unknown as PolymeshTransaction<void>;
const args = {
signers: [account],
Expand All @@ -284,9 +287,26 @@ describe('MultiSig class', () => {
.calledWith({ args: { multiSig, ...args }, transformer: undefined }, context, {})
.mockResolvedValue(expectedTransaction);

const queue = await multiSig.modify(args);
const procedure = await multiSig.modify(args);

expect(procedure).toBe(expectedTransaction);
});
});

describe('method: joinCreator', () => {
it('should prepare the procedure and return the resulting procedure', async () => {
const expectedTransaction = 'someTransaction' as unknown as PolymeshTransaction<void>;
const args = {
asPrimary: true,
};

when(procedureMockUtils.getPrepareMock())
.calledWith({ args: { multiSig, ...args }, transformer: undefined }, context, {})
.mockResolvedValue(expectedTransaction);

const procedure = await multiSig.joinCreator(args);

expect(queue).toBe(expectedTransaction);
expect(procedure).toBe(expectedTransaction);
});
});
});
24 changes: 24 additions & 0 deletions src/api/entities/Account/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
TxTags,
UnsubCallback,
} from '~/types';
import { tuple } from '~/types/utils';
import * as utilsConversionModule from '~/utils/conversion';
import * as utilsInternalModule from '~/utils/internal';

Expand Down Expand Up @@ -1016,4 +1017,27 @@ describe('Account class', () => {
return expect(account.getPendingProposals()).rejects.toThrow(expectedError);
});
});

describe('method: getOffChainReceipts', () => {
it('should return the list of off chain receipts redeemed by the Account', async () => {
const mockResult = [new BigNumber(1), new BigNumber(2)];

const accountId = dsMockUtils.createMockAccountId(address);
jest.spyOn(utilsConversionModule, 'stringToAccountId').mockReturnValue(accountId);

const u64ToBigNumberSpy = jest.spyOn(utilsConversionModule, 'u64ToBigNumber');
mockResult.forEach(uid => {
when(u64ToBigNumberSpy).calledWith(dsMockUtils.createMockU64(uid)).mockReturnValue(uid);
});

dsMockUtils.createQueryMock('settlement', 'receiptsUsed', {
entries: mockResult.map(uid =>
tuple([accountId, dsMockUtils.createMockU64(uid)], dsMockUtils.createMockBool(true))
),
});

const result = await account.getOffChainReceipts();
expect(result).toEqual(mockResult);
});
});
});
29 changes: 29 additions & 0 deletions src/api/entities/Account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
stringToHash,
txTagToExtrinsicIdentifier,
u32ToBigNumber,
u64ToBigNumber,
} from '~/utils/conversion';
import {
assertAddressValid,
Expand Down Expand Up @@ -339,6 +340,7 @@ export class Account extends Entity<UniqueIdentifiers, string> {
throw new PolymeshError({
code: ErrorCode.DataUnavailable,
message: 'There is no Identity associated with this Account',
data: { address },
});
}

Expand Down Expand Up @@ -580,4 +582,31 @@ export class Account extends Entity<UniqueIdentifiers, string> {

return multiSig.getProposals();
}

/**
* Returns all off chain receipts used by this Account
*/
public async getOffChainReceipts(): Promise<BigNumber[]> {
const {
context: {
polymeshApi: {
query: {
settlement: { receiptsUsed },
},
},
},
address,
context,
} = this;

const rawReceiptEntries = await receiptsUsed.entries(stringToAccountId(address, context));

return rawReceiptEntries.map(
([
{
args: [, rawReceiptUid],
},
]) => u64ToBigNumber(rawReceiptUid)
);
}
}
10 changes: 7 additions & 3 deletions src/api/entities/Asset/Base/Metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,19 @@ export class Metadata extends Namespace<BaseAsset> {
context: {
polymeshApi: {
query: {
asset: { assetMetadataNextLocalKey },
asset: { currentAssetMetadataLocalKey },
},
},
},
} = this;

const rawId = await assetMetadataNextLocalKey(ticker);
const rawId = await currentAssetMetadataLocalKey(ticker);

return u64ToBigNumber(rawId).plus(1); // "next" is actually the last used
if (rawId.isSome) {
return u64ToBigNumber(rawId.unwrap()).plus(1);
}

return new BigNumber(1);
}

/**
Expand Down
15 changes: 12 additions & 3 deletions src/api/entities/Asset/__tests__/Base/Metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,23 @@ describe('Metadata class', () => {
jest.restoreAllMocks();
});

it('should return the MetadataEntry for requested id and type', async () => {
dsMockUtils.createQueryMock('asset', 'assetMetadataNextLocalKey', {
returnValue: rawId,
it('should return the next local Metadata ID', async () => {
dsMockUtils.createQueryMock('asset', 'currentAssetMetadataLocalKey', {
returnValue: dsMockUtils.createMockOption(rawId),
});

const result = await metadata.getNextLocalId();
expect(result).toEqual(new BigNumber(2));
});

it('should return the next local Metadata ID as 1 for assets with no existing local metadata', async () => {
dsMockUtils.createQueryMock('asset', 'currentAssetMetadataLocalKey', {
returnValue: dsMockUtils.createMockOption(),
});

const result = await metadata.getNextLocalId();
expect(result).toEqual(new BigNumber(1));
});
});

describe('method: getDetails', () => {
Expand Down
18 changes: 9 additions & 9 deletions src/api/entities/Identity/__tests__/Portfolios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,16 +382,16 @@ describe('Portfolios class', () => {
expect(result[1].blockNumber).toEqual(blockNumber2);
expect(result[0].blockHash).toBe(blockHash1);
expect(result[1].blockHash).toBe(blockHash2);
expect(result[0].legs[0].asset.ticker).toBe(ticker1);
expect(result[1].legs[0].asset.ticker).toBe(ticker2);
expect((result[0].legs[0] as FungibleLeg).asset.ticker).toBe(ticker1);
expect((result[1].legs[0] as FungibleLeg).asset.ticker).toBe(ticker2);
expect((result[0].legs[0] as FungibleLeg).amount).toEqual(amount1.div(Math.pow(10, 6)));
expect((result[1].legs[0] as FungibleLeg).amount).toEqual(amount2.div(Math.pow(10, 6)));
expect(result[0].legs[0].from.owner.did).toBe(portfolioDid1);
expect(result[0].legs[0].to.owner.did).toBe(portfolioDid2);
expect((result[0].legs[0] as FungibleLeg).from.owner.did).toBe(portfolioDid1);
expect((result[0].legs[0] as FungibleLeg).to.owner.did).toBe(portfolioDid2);
expect((result[0].legs[0].to as NumberedPortfolio).id).toEqual(portfolioId2);
expect(result[1].legs[0].from.owner.did).toBe(portfolioDid2);
expect((result[1].legs[0] as FungibleLeg).from.owner.did).toBe(portfolioDid2);
expect((result[1].legs[0].from as NumberedPortfolio).id).toEqual(portfolioId2);
expect(result[1].legs[0].to.owner.did).toEqual(portfolioDid1);
expect((result[1].legs[0] as FungibleLeg).to.owner.did).toEqual(portfolioDid1);
expect(result[2].legs[0].direction).toEqual(SettlementDirectionEnum.None);
expect(result[3].legs[0].direction).toEqual(SettlementDirectionEnum.None);

Expand Down Expand Up @@ -446,10 +446,10 @@ describe('Portfolios class', () => {

expect(result[0].blockNumber).toEqual(blockNumber1);
expect(result[0].blockHash).toBe(blockHash1);
expect(result[0].legs[0].asset.ticker).toBe(ticker2);
expect((result[0].legs[0] as FungibleLeg).asset.ticker).toBe(ticker2);
expect((result[0].legs[0] as FungibleLeg).amount).toEqual(amount2.div(Math.pow(10, 6)));
expect(result[0].legs[0].from.owner.did).toBe(portfolioDid1);
expect(result[0].legs[0].to.owner.did).toBe(portfolioDid1);
expect((result[0].legs[0] as FungibleLeg).from.owner.did).toBe(portfolioDid1);
expect((result[0].legs[0] as FungibleLeg).to.owner.did).toBe(portfolioDid1);
expect((result[0].legs[0].to as NumberedPortfolio).id).toEqual(portfolioId2);
});
});
Expand Down
Loading
Loading