Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
213 changes: 213 additions & 0 deletions packages/api/test/e2e/doughnut-v0.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2019 Centrality Investments Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import testingPairs from '@plugnet/keyring/testingPairs';
import { hexToU8a, assert } from '@plugnet/util'
import { encode as encodeCennznut } from '@cennznet/cennznut';
import { generate as encodeDoughnut } from '@plugnet/doughnut-maker';

import { Api } from '../../src/Api';
import { Keypair } from '@plugnet/util-crypto/types';
import { KeyringPair } from '@cennznet/util/types';
import { Extrinsic } from '@cennznet/types/extrinsic';

/// Helper for creating CENNZnuts
function makeCennznut(module: string, method: string): Uint8Array {
return encodeCennznut(0, {
"modules": {
[module]: {
"methods": {
[method]: {}
}
}
}
});
}

/// Helper for creating v0 Doughnuts
async function makeDoughnut(
issuer: Keypair,
holder: KeyringPair,
permissions: Record<string, Uint8Array>,
): Promise<Uint8Array> {
return await encodeDoughnut(
0,
0,
{
issuer: issuer.publicKey,
holder: holder.publicKey,
expiry: 55555,
block_cooldown: 0,
permissions: permissions,
},
issuer
);
}

describe.skip('Doughnut for CennznetExtrinsic', () => {
let aliceKeyPair = {
secretKey: hexToU8a('0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011'),
publicKey: hexToU8a('0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d')
};
let api: Api;
let keyring: {
[index: string]: KeyringPair;
};

beforeAll(async () => {
api = await Api.create({provider: 'wss://rimu.unfrastructure.io/public/ws'});
keyring = testingPairs({ type: 'sr25519' });
});

afterEach(() => {
jest.setTimeout(5000);
});

it('Delegates a GA transfer from alice to charlie when extrinsic is signed by bob', async done => {

let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("generic-asset", "transfer") }
);

const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx.addDoughnut(doughnut);

const opt = {doughnut};

await tx.signAndSend(keyring.bob, opt, async ({events, status}) => {
if (status.isFinalized) {
const transfer = events.find(
event => (
event.event.data.method === 'Transferred' &&
event.event.data.section === 'genericAsset' &&
event.event.data[1].toString() === keyring.alice.address // transferred from alice's account
)
);
if (transfer != undefined) {
done();
} else {
assert(true, "false");
}
}
});
});

it('Fails when charlie uses bob\'s doughnut', async () => {
let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("generic-asset", "transfer") }
);
const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx.addDoughnut(doughnut);

await expect(tx.signAndSend(keyring.charlie, () => { })).rejects.toThrow("1010: Invalid Transaction (0)");
});

it('fails when cennznut does not authorize the extrinsic method', async (done) => {
let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("generic-asset", "mint") }
);

const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx.addDoughnut(doughnut);

await tx.signAndSend(keyring.bob, async ({events, status}) => {
if (status.isFinalized) {
const failed = events.find(event => event.event.data.method === 'ExtrinsicFailed');
if (failed != undefined) {
done();
} else {
assert(false, "expected extrinsic to fail");
}
}
});

});

it('fails when cennznut does not authorize the extrinsic module', async (done) => {
let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("balance", "transfer") }
);

const tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx.addDoughnut(doughnut);

await tx.signAndSend(keyring.bob, async ({events, status}) => {
if (status.isFinalized) {
const failed = events.find(event => event.event.data.method === 'ExtrinsicFailed');
if (failed != undefined) {
done();
} else {
assert(false, "expected extrinsic to fail");
}
}
});

});

it('can decode a doughnut from a signed extrinsic', async () => {
let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("generic-asset", "transfer") }
);

let tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx = tx.addDoughnut(doughnut);
let signed = tx.sign(keyring.bob, {});

let original_extrinsic = tx as unknown as Extrinsic;
let new_extrinsic = new Extrinsic(signed.toHex());
assert(
new_extrinsic.doughnut.toHex() === original_extrinsic.doughnut.toHex(),
"doughnut does not match decoded version"
);
assert(new_extrinsic.toHex() === original_extrinsic.toHex(), "extrinsics do not match");

});

it('can decode a doughnut from a signed extrinsic with fee exchange', async () => {
let doughnut = await makeDoughnut(
aliceKeyPair,
keyring.bob,
{ "cennznet": makeCennznut("generic-asset", "transfer") }
);

let tx = api.tx.genericAsset.transfer(16001, keyring.charlie.address, 10000);
tx = tx.addFeeExchangeOpt({
assetId: 17000,
maxPayment: '12345',
});
tx = tx.addDoughnut(doughnut);
let signed = tx.sign(keyring.alice, {});

let original_extrinsic = signed as unknown as Extrinsic;
let new_extrinsic = new Extrinsic(signed.toHex());

assert(
new_extrinsic.doughnut.toHex() === original_extrinsic.doughnut.toHex(),
"doughnut does not match decoded version"
);
assert(new_extrinsic.toHex() === original_extrinsic.toHex(), "extrinsics do not match");

});

});
4 changes: 2 additions & 2 deletions packages/rpc-core/src/jsonrpc.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export interface RpcInterface {
};
state: {
call(method: Text | string, data: Bytes | Uint8Array | string, block?: Hash | Uint8Array | string): Observable<Bytes>;
getChildKeys(childStorageKey: any, prefix: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;
getChildKeys(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

getChildStorage(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<StorageData>;
getChildStorageHash(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<Hash>;
getChildStorageSize(childStorageKey: any, key: any, block?: Hash | Uint8Array | string): Observable<u64>;
getKeys(prefix: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;
getKeys(key: any, block?: Hash | Uint8Array | string): Observable<Vec<StorageKey>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

getMetadata(block?: Hash | Uint8Array | string): Observable<Metadata>;
getRuntimeVersion(hash?: Hash | Uint8Array | string): Observable<RuntimeVersion>;
getStorage<T = Codec>(key: any, block?: Hash | Uint8Array | string): Observable<T>;
Expand Down
43 changes: 43 additions & 0 deletions packages/types/src/primitive/Doughnut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2019 Centrality Investments Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {Bytes, Compact, U8a} from '@plugnet/types';
import {AnyU8a} from '@plugnet/types/types';

/**
* An encoded, signed v0 Doughnut certificate
**/
export default class Doughnut extends U8a {
get encodedLength(): number {
return this.toU8a().length;
}

constructor(value?: AnyU8a) {
// This function is used as both a constructor and a decoder
// Doughnut has its own codec but it must be length prefixed to support the SCALE codec used by the extrinsic

// Failure to decode indicates a call as a constructor
const decoded = new Bytes(value);
if (decoded.length > 0) {
super(decoded);
} else {
super(value);
}
}

toU8a(isBare?: boolean): Uint8Array {
// Encode the doughnut with length prefix to support SCALE codec
return isBare ? (this as Uint8Array) : Compact.addLengthPrefix(this);
}
}
1 change: 1 addition & 0 deletions packages/types/src/primitive/Extrinsic/Extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export default class Extrinsic extends Base<ExtrinsicVx | ExtrinsicUnknown> impl
// FIXME The old support as detailed above... needs to be dropped
if ((args as any[]).length === 2) {
payload = {
doughnut: new Uint8Array(),
blockHash: new Uint8Array(),
era: (args as any[])[1] as string,
genesisHash: new Uint8Array(),
Expand Down
12 changes: 10 additions & 2 deletions packages/types/src/primitive/Extrinsic/SignerPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import { u8aToHex } from '@plugnet/util';
import { Address, Balance, BlockNumber, Call, ExtrinsicEra, Hash, Index, RuntimeVersion } from '../../interfaces';
import Compact from '../../codec/Compact';
import Struct from '../../codec/Struct';
import Option from '../../codec/Option';
import { createType } from '../../codec';
import { Codec, Constructor, ISignerPayload, SignerPayloadJSON, SignerPayloadRaw } from '../../types';
import u8 from '../U8';
import Doughnut from '../Doughnut';

export interface SignerPayloadType extends Codec {
address: Address;
doughnut?: Option<Doughnut>;
blockHash: Hash;
blockNumber: BlockNumber;
era: ExtrinsicEra;
Expand All @@ -27,6 +30,7 @@ export interface SignerPayloadType extends Codec {
// We can ignore the properties, added via Struct.with
const _Payload: Constructor<SignerPayloadType> = Struct.with({
address: 'Address',
doughnut: Option.with(Doughnut),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you use 'Option<Doughnut>' which will allow user replace the default implementation

blockHash: 'Hash',
blockNumber: 'BlockNumber',
era: 'ExtrinsicEra',
Expand All @@ -43,9 +47,9 @@ export default class SignerPayload extends _Payload implements ISignerPayload {
* @description Creates an representation of the structure as an ISignerPayload JSON
*/
public toPayload (): SignerPayloadJSON {
const { address, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this;
const { address, doughnut, blockHash, blockNumber, era, genesisHash, method, nonce, runtimeVersion: { specVersion }, tip, version } = this;

return {
const ret: SignerPayloadJSON = {
address: address.toString(),
blockHash: blockHash.toHex(),
blockNumber: blockNumber.toHex(),
Expand All @@ -57,6 +61,10 @@ export default class SignerPayload extends _Payload implements ISignerPayload {
tip: tip.toHex(),
version: version.toNumber()
};
if (doughnut.isSome) {
ret.doughnut = doughnut.unwrap().toHex();
}
return ret;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/primitive/Extrinsic/v2/ExtrinsicPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { ExtrinsicPayloadValue, IKeyringPair, InterfaceTypes } from '../../../ty
import Compact from '../../../codec/Compact';
import Struct from '../../../codec/Struct';
import Bytes from '../../../primitive/Bytes';
import Doughnut from '../../../primitive/Doughnut';
import { sign } from '../util';

// SignedExtra adds the following fields to the payload
const SignedExtraV2: Record<string, InterfaceTypes> = {
//Option<<Runtime as system::Trait>::Doughnut>,
doughnut: 'Doughnut',
// system::CheckEra<Runtime>
blockHash: 'Hash'
// system::CheckNonce<Runtime>
Expand All @@ -36,6 +39,13 @@ export default class ExtrinsicPayloadV2 extends Struct {
}, value);
}

/**
* @description Doughnut [[Doughnut]] attached permission proof.
*/
public get doughnut (): Doughnut {
return this.get('doughnut') as Doughnut;
}

/**
* @description The block [[Hash]] the signature applies to (mortal/immortal)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default class ExtrinsicSignatureV2 extends Struct implements IExtrinsicSi
/**
* @description Generate a payload and pplies the signature from a keypair
*/
public sign (method: Call, account: IKeyringPair, { blockHash, era, genesisHash, nonce, tip }: SignatureOptions): IExtrinsicSignature {
public sign (method: Call, account: IKeyringPair, { doughnut, blockHash, era, genesisHash, nonce, tip }: SignatureOptions): IExtrinsicSignature {
const signer = createType('Address', account.publicKey);
const payload = new ExtrinsicPayloadV2({
blockHash,
Expand All @@ -126,6 +126,9 @@ export default class ExtrinsicSignatureV2 extends Struct implements IExtrinsicSi
specVersion: 0, // unused for v2
tip: tip || 0
});
if (doughnut.isSome) {
payload.doughnut = doughnut;
}
const signature = createType('Signature', payload.sign(account));

return this.injectSignature(signer, signature, payload);
Expand Down
Loading