# EPAM Blockchain Professionals Workshop https://wearecommunity.io/events/dappletsprojectworkshop ## Short Plan 1. Explain what is a dapplet, adapter, extension. How it works. 2. Show the final version of the dapplet and explain what we will do. 3. Install Dapplet Extension. Show some dapplets. 4. Clone the GitHub Dapplet repository. 5. Describe the environment and install dependencies. 6. Try to run the working dapplet. 7. Switch to exercise branch 8. Explane what parts the application consists of. 9. Adapter: why we need it, what parts it has. 10. Dapplet - the application for users. 11. Show the overlay. Explay what is the difference with an ordinary React SPA. 12. Show contracts. 13. Run the created dapplet. 14. Deploy it to the registry. ## Detailed Plan 1. **Explain what is a dapplet, adapter, extension. How it works.** ![](https://i.imgur.com/5KG9pWN.jpg) 2. **Show the final version of the dapplet and explain what we will do.** ![](https://i.imgur.com/ReymLKF.png) 3. **Install Dapplet Extension. Show some dapplets.** https://docs.dapplets.org/docs/installation 4. **Clone the GitHub Dapplet repository.** https://github.com/dapplets/github-dapplet-workshop Or you can use GitPod: https://gitpod.io/#https://github.com/dapplets/github-dapplet-workshop/tree/main 5. **Describe the environment and install dependencies.** * Node.js ^16 (you can try ^14) * npm * typescript * lit-plugin (VSCode extension) * solidity (VSCode extension) 6. **Try to run the working dapplet.** ``` npm i npm start ``` On the Developer tab of the Extension connect two Development Servers: * http://localhost:3001/dapplet.json (http!!!) * http://localhost:3002/dapplet.json (http!!!) ![](https://i.imgur.com/SzRAkrl.png) If you are using GitPod there are different urls. You need to find the links above in the console, click to them and add new addreses to the development servers list. Go to the Dapplet tab and turn on the dapplet. It's important to be on the GitHub page. ![](https://i.imgur.com/njgZ8Ib.png) 7. **Stop the application in the VSCode and switch to exercise branch** 8. **Explane what parts the application consists of** There are 5 components in our dapplet: ![](https://i.imgur.com/2NLqX2m.png) 9. **Adapter: why we need it, what parts it has.** 9.1. **Widget: web component, props, insertion points** 9.2. **Adapter's class: what is a template and what we should change.** 9.3. **Implement adapters config:** Import the widget ```typescript import { Button } from './button'; ``` Add it to adapter's Exports ```typescript public exports = (): Exports => ({ button: this.adapter.createWidgetFactory(Button), }); ``` Implement the adapter's config ```typescript public config = { ISSUE_COMMENT: { containerSelector: '.js-discussion.js-socket-channel', contextSelector: '.TimelineItem.js-comment-container', insPoints: { HEADER_BUTTONS: { selector: '.timeline-comment-actions', insert: 'end' }, }, // eslint-disable-next-line @typescript-eslint/no-explicit-any contextBuilder: (searchNode: any): ContextBuilder => ({ id: searchNode.querySelector('.timeline-comment-group')?.id, page: document.location.origin + document.location.pathname, }), }, }; ``` 10. **Dapplet - the application for users.** 10.1. **Main structure** ```typescript import { } from '@dapplets/dapplet-extension'; @Injectable export default class GitHubDapplet { @Inject('') public adapter: any; // specify adapter async activate(): Promise<void> { const { } = this.adapter.exports; // get widgets this.adapter.attachConfig( // add config ); } } ``` 10.2. **Add the button. Run the dapplet.** ```typescript import { } from '@dapplets/dapplet-extension'; import LOGO from './icons/logo.svg'; @Injectable export default class GitHubDapplet { @Inject('github-adapter-example.dapplet-base.eth') public adapter: any; async activate(): Promise<void> { const { button } = this.adapter.exports; this.adapter.attachConfig({ ISSUE_COMMENT: (ctx) => button({ DEFAULT: { img: LOGO, label: 0, } }) }); } } ``` 10.3. **Let's add a dapplet's state.** It is a shared state for the dapplet and the overlay. For using it in the overlay it should use `dapplet-overlay-bridge`. We will talk about this later. To add a state we need an interface that describes the structure of the data we want to store. Let's call is IStorage interface. ```typescript interface IStorage { likes: string[] counter: number link: string isActive: boolean userAccount: string } ``` ```typescript // STATE const state = Core.state<IStorage>( { likes: [], counter: 0, link: '', isActive: false, userAccount: '' } ); ``` 10.4. **Get account ID** In the full version of the dapplet, which you can find in the main branch, you can choose Ethereum or NEAR wallet and contract interactions by committing some lines of code (marked // NEAR or // Ethereum). Now we will implement only Ethereum part. Let's go back to the code. We will get and store an account ID if we've already been connected to the wallet. ```typescript // GET ACCOUNT ID const prevSessions = await Core.sessions(); const prevSession = prevSessions.find(x => x.authMethod === 'ethereum/goerli'); if (prevSession) { const wallet = await prevSession.wallet(); const accountIds = await wallet.request({ method: 'eth_accounts', params: [] }); state.global.userAccount.next(accountIds[0]); } ``` 10.5. **Here we implement interaction with the contract: connect and get all the data.** Firstly we should import ABI ```typescript import ABI from './ABI'; ``` Then we use `Core.contract()` method to connect to the contract. ```typescript // CONTRACTS const ethereumContract = await Core.contract( 'ethereum', '0xEd087354AB06D15Bc03F4F2DB52B22A14e7A9AF9', ABI ); const allCommentsFromContract = await ethereumContract.getAll(); console.log('allCommentsFromContract', allCommentsFromContract); ``` Then save it to the dapplet's state. ```typescript const currentAccount = state.global.userAccount.value.toLowerCase(); for (const pair of allCommentsFromContract) { const [key, value] = pair; const [link, id] = key.split('#'); state[id].likes.next(value); state[id].counter.next(value.length); state[id].link.next(key); state[id].isActive.next( value.map(x => x.toLowerCase()).includes(currentAccount) ); } ``` 10.6. **Create the overlay class instance.** This object gives us the possibility to create a tab in our extension's overlay for our dapplet and communicate with it. At first we should implemtnt some functions that will be available into the overlay. In our example we need two functions: login and logout. We can show it in the interface ```typescript interface IBridge { login: () => Promise<void> logout: () => Promise<void> } ``` Also I added an addition function that update the dapplet's state. It placed in the bottom of the document. Just uncommet it. ```typescript const changeIsActiveStates = (state: any) => { const commentsInState = state.getAll(); delete commentsInState.global; const currentAccount = state.global.userAccount.value.toLowerCase(); Object.entries(commentsInState).forEach(([id, commentData]: [id: string, commentData: IStorage]) => { if (commentData.likes.map(x => x.toLowerCase()).includes(currentAccount) !== commentData.isActive) { state[id].isActive.next(!commentData.isActive); } }); } ``` Now we can implement login and logout functions ```typescript const login = async () => { try { const existSessions = await Core.sessions(); const existSession = existSessions.find(x => x.authMethod === 'ethereum/goerli'); const session = existSession ?? await Core.login({ authMethods: ['ethereum/goerli'], target: overlay }); const wallet = await session.wallet(); const accountIds = await wallet.request({ method: 'eth_accounts', params: [] }); state.global.userAccount.next(accountIds[0]); changeIsActiveStates(state); } catch (err) { console.log('Login was denied', err); } }; const logout = async () => { const sessions = await Core.sessions(); sessions.forEach(x => x.logout()); state.global.userAccount.next(''); changeIsActiveStates(state); }; ``` Now we have two shared functions and shared state. So we can create the overlay. We will use Core method `Core.overlay<>()`. Then we attach state and our functions using `.useState()` and `.declare()` methods. ```typescript // OVERLAY const overlay = Core.overlay<IBridge>({ name: 'github-dapplet-overlay', title: 'GitHub Dapplet' }).useState(state) .declare({ login, logout }); Core.onAction(() => overlay.open()); ``` 10.7. **Change the button's label and add isActive property** ```typescript this.adapter.attachConfig({ ISSUE_COMMENT: (ctx) => button({ initial: 'DEFAULT', DEFAULT: { label: state[ctx.id].counter, img: LOGO, isActive: state[ctx.id].isActive, } }) }) ``` These values are observable objects so they will be updated automatically 10.8. **Add to the button exec function** This function will be executed on button click. ```typescript exec: async () => { // ... } ``` Only logged in users can vote. So we have to check this and log in if needed. ```typescript if (name === '') { await login(); name = state.global.userAccount.value.toLowerCase(); } ``` Then we need two functions: add like and remove like ```typescript const addLike = async () => { try { await ethereumContract.addLike(ctx.page + '#' + ctx.id); const newValue = [...likes.value, name]; likes.next(newValue); counter.next(counter.value + 1); link.next(ctx.page + '#' + ctx.id); isActive.next(true); } catch (err) { console.log('Error calling addLike():', err); } }; const removeLike = async () => { try { await ethereumContract.removeLike(ctx.page + '#' + ctx.id); const newValue = likes.value.filter(x => x.toLowerCase() !== name); likes.next(newValue); counter.next(counter.value - 1); isActive.next(false); } catch (err) { console.log('Error calling removeLike():', err); } } ``` And the last is to call one of these functions. It depends on if we already liked this comment or not. ```typescript if (!likes.value.map(x => x.toLowerCase()).includes(name)) { addLike(); } else { removeLike(); } ``` This is the fulll exec function: ```typescript exec: async () => { let name = state.global.userAccount.value.toLowerCase(); if (name === '') { await login(); name = state.global.userAccount.value.toLowerCase(); } const { likes, counter, link, isActive } = state[ctx.id]; const addLike = async () => { try { await ethereumContract.addLike(ctx.page + '#' + ctx.id); const newValue = [...likes.value, name]; likes.next(newValue); counter.next(counter.value + 1); link.next(ctx.page + '#' + ctx.id); isActive.next(true); } catch (err) { console.log('Error calling addLike():', err); } }; const removeLike = async () => { try { await ethereumContract.removeLike(ctx.page + '#' + ctx.id); const newValue = likes.value.filter(x => x.toLowerCase() !== name); likes.next(newValue); counter.next(counter.value - 1); isActive.next(false); } catch (err) { console.log('Error calling removeLike():', err); } } if (!likes.value.map(x => x.toLowerCase()).includes(name)) { addLike(); } else { removeLike(); } }, ``` 11. **Show the overlay. Explain what is the difference with an ordinary React SPA.** 12. **Show contracts.** 13. **Run the created dapplet.** 14. **Deploy it to the registry.** # References * Extension: https://github.com/dapplets/dapplet-extension/releases/tag/v0.49.0 (download Browser Extension (zip)) * Installation: https://docs.dapplets.org/docs/installation * Clone this repo: https://github.com/dapplets/github-dapplet-workshop It has two branches: exercise - the start point for our workshop and main - the working dapplet. * To learn more about Dapplets Project, visit https://dapplets.org/promo * YouTube stream: https://www.youtube.com/watch?v=gB0-iBByXuA