|
1 | 1 | import type { AccountSigner } from '@metamask/7715-permission-types';
|
2 | 2 | import { Messenger, deriveStateFromMetadata } from '@metamask/base-controller';
|
| 3 | +import { |
| 4 | + createTimestampTerms, |
| 5 | + createNativeTokenStreamingTerms, |
| 6 | + ROOT_AUTHORITY, |
| 7 | +} from '@metamask/delegation-core'; |
| 8 | +import { |
| 9 | + CHAIN_ID, |
| 10 | + DELEGATOR_CONTRACTS, |
| 11 | +} from '@metamask/delegation-deployments'; |
3 | 12 | import type { HandleSnapRequest, HasSnap } from '@metamask/snaps-controllers';
|
4 | 13 | import type { SnapId } from '@metamask/snaps-sdk';
|
5 |
| -import type { Hex } from '@metamask/utils'; |
| 14 | +import { hexToBigInt, numberToHex, type Hex } from '@metamask/utils'; |
6 | 15 |
|
| 16 | +import { DELEGATION_FRAMEWORK_VERSION } from './decodePermission'; |
7 | 17 | import type { GatorPermissionsControllerMessenger } from './GatorPermissionsController';
|
8 | 18 | import GatorPermissionsController from './GatorPermissionsController';
|
9 | 19 | import {
|
@@ -453,6 +463,215 @@ describe('GatorPermissionsController', () => {
|
453 | 463 | `);
|
454 | 464 | });
|
455 | 465 | });
|
| 466 | + |
| 467 | + describe('decodePermissionFromPermissionContextForOrigin', () => { |
| 468 | + const chainId = CHAIN_ID.sepolia; |
| 469 | + const contracts = |
| 470 | + DELEGATOR_CONTRACTS[DELEGATION_FRAMEWORK_VERSION][chainId]; |
| 471 | + |
| 472 | + const delegatorAddressA = |
| 473 | + '0x1111111111111111111111111111111111111111' as Hex; |
| 474 | + const delegateAddressB = |
| 475 | + '0x2222222222222222222222222222222222222222' as Hex; |
| 476 | + const metamaskOrigin = 'https://metamask.io'; |
| 477 | + const buildMetadata = (justification: string) => ({ |
| 478 | + justification, |
| 479 | + origin: metamaskOrigin, |
| 480 | + }); |
| 481 | + |
| 482 | + let controller: GatorPermissionsController; |
| 483 | + |
| 484 | + beforeEach(() => { |
| 485 | + controller = new GatorPermissionsController({ |
| 486 | + messenger: getMessenger(), |
| 487 | + }); |
| 488 | + }); |
| 489 | + |
| 490 | + it('decodes a native-token-stream permission successfully', async () => { |
| 491 | + const { |
| 492 | + TimestampEnforcer, |
| 493 | + NativeTokenStreamingEnforcer, |
| 494 | + ExactCalldataEnforcer, |
| 495 | + NonceEnforcer, |
| 496 | + } = contracts; |
| 497 | + |
| 498 | + const delegator = delegatorAddressA; |
| 499 | + const delegate = delegateAddressB; |
| 500 | + |
| 501 | + const timestampBeforeThreshold = 1720000; |
| 502 | + const expiryTerms = createTimestampTerms( |
| 503 | + { timestampAfterThreshold: 0, timestampBeforeThreshold }, |
| 504 | + { out: 'hex' }, |
| 505 | + ); |
| 506 | + |
| 507 | + const initialAmount = 123456n; |
| 508 | + const maxAmount = 999999n; |
| 509 | + const amountPerSecond = 1n; |
| 510 | + const startTime = 1715664; |
| 511 | + const streamTerms = createNativeTokenStreamingTerms( |
| 512 | + { initialAmount, maxAmount, amountPerSecond, startTime }, |
| 513 | + { out: 'hex' }, |
| 514 | + ); |
| 515 | + |
| 516 | + const caveats = [ |
| 517 | + { |
| 518 | + enforcer: TimestampEnforcer, |
| 519 | + terms: expiryTerms, |
| 520 | + args: '0x', |
| 521 | + } as const, |
| 522 | + { |
| 523 | + enforcer: NativeTokenStreamingEnforcer, |
| 524 | + terms: streamTerms, |
| 525 | + args: '0x', |
| 526 | + } as const, |
| 527 | + { enforcer: ExactCalldataEnforcer, terms: '0x', args: '0x' } as const, |
| 528 | + { enforcer: NonceEnforcer, terms: '0x', args: '0x' } as const, |
| 529 | + ]; |
| 530 | + |
| 531 | + const delegation = { |
| 532 | + delegate, |
| 533 | + delegator, |
| 534 | + authority: ROOT_AUTHORITY as Hex, |
| 535 | + caveats, |
| 536 | + }; |
| 537 | + |
| 538 | + const result = |
| 539 | + await controller.decodePermissionFromPermissionContextForOrigin({ |
| 540 | + origin: controller.permissionsProviderSnapId, |
| 541 | + chainId, |
| 542 | + delegation, |
| 543 | + metadata: buildMetadata('Test justification'), |
| 544 | + }); |
| 545 | + |
| 546 | + expect(result.chainId).toBe(numberToHex(chainId)); |
| 547 | + expect(result.address).toBe(delegator); |
| 548 | + expect(result.signer).toStrictEqual({ |
| 549 | + type: 'account', |
| 550 | + data: { address: delegate }, |
| 551 | + }); |
| 552 | + expect(result.permission.type).toBe('native-token-stream'); |
| 553 | + expect(result.expiry).toBe(timestampBeforeThreshold); |
| 554 | + // amounts are hex-encoded in decoded data; startTime is numeric |
| 555 | + expect(result.permission.data.startTime).toBe(startTime); |
| 556 | + // BigInt fields are encoded as hex; compare after decoding |
| 557 | + expect(hexToBigInt(result.permission.data.initialAmount)).toBe( |
| 558 | + initialAmount, |
| 559 | + ); |
| 560 | + expect(hexToBigInt(result.permission.data.maxAmount)).toBe(maxAmount); |
| 561 | + expect(hexToBigInt(result.permission.data.amountPerSecond)).toBe( |
| 562 | + amountPerSecond, |
| 563 | + ); |
| 564 | + expect(result.permission.justification).toBe('Test justification'); |
| 565 | + }); |
| 566 | + |
| 567 | + it('throws when origin does not match permissions provider', async () => { |
| 568 | + await expect( |
| 569 | + controller.decodePermissionFromPermissionContextForOrigin({ |
| 570 | + origin: 'not-the-provider', |
| 571 | + chainId: 1, |
| 572 | + delegation: { |
| 573 | + delegate: '0x1', |
| 574 | + delegator: '0x2', |
| 575 | + authority: ROOT_AUTHORITY as Hex, |
| 576 | + caveats: [], |
| 577 | + }, |
| 578 | + metadata: buildMetadata(''), |
| 579 | + }), |
| 580 | + ).rejects.toThrow('Origin not-the-provider not allowed'); |
| 581 | + }); |
| 582 | + |
| 583 | + it('throws when enforcers do not identify a supported permission', async () => { |
| 584 | + const { TimestampEnforcer, ValueLteEnforcer } = contracts; |
| 585 | + |
| 586 | + const expiryTerms = createTimestampTerms( |
| 587 | + { timestampAfterThreshold: 0, timestampBeforeThreshold: 100 }, |
| 588 | + { out: 'hex' }, |
| 589 | + ); |
| 590 | + |
| 591 | + const caveats = [ |
| 592 | + { |
| 593 | + enforcer: TimestampEnforcer, |
| 594 | + terms: expiryTerms, |
| 595 | + args: '0x', |
| 596 | + } as const, |
| 597 | + // Include a forbidden/irrelevant enforcer without required counterparts |
| 598 | + { enforcer: ValueLteEnforcer, terms: '0x', args: '0x' } as const, |
| 599 | + ]; |
| 600 | + |
| 601 | + await expect( |
| 602 | + controller.decodePermissionFromPermissionContextForOrigin({ |
| 603 | + origin: controller.permissionsProviderSnapId, |
| 604 | + chainId, |
| 605 | + delegation: { |
| 606 | + delegate: delegatorAddressA, |
| 607 | + delegator: delegateAddressB, |
| 608 | + authority: ROOT_AUTHORITY as Hex, |
| 609 | + caveats, |
| 610 | + }, |
| 611 | + metadata: buildMetadata(''), |
| 612 | + }), |
| 613 | + ).rejects.toThrow('Failed to decode permission'); |
| 614 | + }); |
| 615 | + |
| 616 | + it('throws when authority is not ROOT_AUTHORITY', async () => { |
| 617 | + const { |
| 618 | + TimestampEnforcer, |
| 619 | + NativeTokenStreamingEnforcer, |
| 620 | + ExactCalldataEnforcer, |
| 621 | + NonceEnforcer, |
| 622 | + } = contracts; |
| 623 | + |
| 624 | + const delegator = delegatorAddressA; |
| 625 | + const delegate = delegateAddressB; |
| 626 | + |
| 627 | + const timestampBeforeThreshold = 2000; |
| 628 | + const expiryTerms = createTimestampTerms( |
| 629 | + { timestampAfterThreshold: 0, timestampBeforeThreshold }, |
| 630 | + { out: 'hex' }, |
| 631 | + ); |
| 632 | + |
| 633 | + const initialAmount = 1n; |
| 634 | + const maxAmount = 2n; |
| 635 | + const amountPerSecond = 1n; |
| 636 | + const startTime = 1715000; |
| 637 | + const streamTerms = createNativeTokenStreamingTerms( |
| 638 | + { initialAmount, maxAmount, amountPerSecond, startTime }, |
| 639 | + { out: 'hex' }, |
| 640 | + ); |
| 641 | + |
| 642 | + const caveats = [ |
| 643 | + { |
| 644 | + enforcer: TimestampEnforcer, |
| 645 | + terms: expiryTerms, |
| 646 | + args: '0x', |
| 647 | + } as const, |
| 648 | + { |
| 649 | + enforcer: NativeTokenStreamingEnforcer, |
| 650 | + terms: streamTerms, |
| 651 | + args: '0x', |
| 652 | + } as const, |
| 653 | + { enforcer: ExactCalldataEnforcer, terms: '0x', args: '0x' } as const, |
| 654 | + { enforcer: NonceEnforcer, terms: '0x', args: '0x' } as const, |
| 655 | + ]; |
| 656 | + |
| 657 | + const invalidAuthority = |
| 658 | + '0x0000000000000000000000000000000000000000' as Hex; |
| 659 | + |
| 660 | + await expect( |
| 661 | + controller.decodePermissionFromPermissionContextForOrigin({ |
| 662 | + origin: controller.permissionsProviderSnapId, |
| 663 | + chainId, |
| 664 | + delegation: { |
| 665 | + delegate, |
| 666 | + delegator, |
| 667 | + authority: invalidAuthority, |
| 668 | + caveats, |
| 669 | + }, |
| 670 | + metadata: buildMetadata(''), |
| 671 | + }), |
| 672 | + ).rejects.toThrow('Failed to decode permission'); |
| 673 | + }); |
| 674 | + }); |
456 | 675 | });
|
457 | 676 |
|
458 | 677 | /**
|
|
0 commit comments